State management has always been a critical part of building complex Vue.js applications. For years, Vuex has been the official state management library for Vue, offering a centralized way to manage and share data across components. However, as Vue evolved—especially with the introduction of the Composition API in Vue 3—Vuex's boilerplate-heavy approach and steep learning curve started to show limitations.
Enter Pinia, the new official state management library for Vue 3. Designed to be simpler, more intuitive, and fully compatible with both the Composition API and TypeScript, Pinia has quickly become the recommended choice for managing application state in modern Vue apps. It retains the powerful concepts of Vuex—like centralized state, actions, and getters—but wraps them in a much cleaner and developer-friendly API.
In this tutorial, you'll learn how to set up and use Pinia in a Vue 3 project. We'll explore how to define stores, use them in components, persist state, test your stores, and even migrate from Vuex. Whether you're building a small app or a large-scale SPA, mastering Pinia will make your state management simpler and more scalable.
What is Pinia?
Pinia is the official state management library for Vue 3, designed as a modern alternative to Vuex. While it shares similar core concepts—like state
, getters
, and actions
—Pinia embraces simplicity, modularity, and a zero-boilerplate philosophy. It was built from the ground up to work seamlessly with the Composition API and is fully compatible with TypeScript.
✅ Why Pinia Over Vuex?
Here are some of the key reasons why Pinia has become the preferred choice for Vue 3:
-
Zero boilerplate: No need for separate
mutations
—just update state directly within actions. -
Modular stores: Easily split logic into multiple smaller stores.
-
TypeScript-friendly: Full type inference with minimal setup.
-
Devtools integration: Full support for Vue Devtools, including time-travel debugging.
-
Lightweight and intuitive: Smaller bundle size and easier to learn.
🔁 Key Differences Between Pinia and Vuex
Feature | Vuex | Pinia |
---|---|---|
Boilerplate | High | Low |
Mutations | Required | Not used |
Modular Stores | Clunky (namespaced) | Native support |
Composition API | Workarounds needed | Built-in support |
TypeScript Support | Verbose | Fully type-safe and inferred |
Official Status | Deprecated in favor of Pinia | Official Vue 3 state manager |
Pinia’s API is designed to feel like plain JavaScript—no special decorators or weird syntax required. With a shallow learning curve and powerful capabilities, it's now the go-to solution for managing complex state in modern Vue apps.
Setting Up the Project
Before diving into Pinia, we need to create a fresh Vue 3 project and install the required dependencies. For this tutorial, we'll use Vite for a fast and modern development environment, but you can also use Vue CLI if preferred.
🛠 Step 1: Create a Vue 3 Project with Vite
If you don’t have Vite installed globally, run:
npm create vite@latest pinia-demo -- --template vue
cd pinia-demo
npm install
Or with yarn
:
yarn create vite pinia-demo --template vue
cd pinia-demo
yarn
This will scaffold a new Vue 3 app using the Vite + Vue template.
📦 Step 2: Install Pinia
Now, install Pinia with the following command:
npm install pinia
or
yarn add pinia
🧩 Step 3: Register Pinia in Your App
Open main.js
or main.ts
and set up Pinia:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
📁 Step 4: Organize the Store Directory
Create a directory to store all your Pinia stores:
mkdir src/stores
This helps keep your project organized as it grows.
✅ At this point, you’re ready to start creating and using Pinia stores in your Vue 3 project.
Creating Your First Pinia Store
Now that Pinia is installed and registered, let's create your first store to manage application state. We'll build a simple counter store to demonstrate the basics of Pinia’s API: state
, getters
, and actions
.
📁 Step 1: Create a New Store File
Inside your src/stores
directory, create a new file called counter.js
(or counter.ts
if you're using TypeScript):
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
incrementBy(amount) {
this.count += amount
}
}
})
🧠 Key Concepts
-
defineStore()
: Creates a new Pinia store. The first parameter is a unique name (used in devtools and SSR). -
state()
: A function that returns the reactive state object. -
getters
: Computed properties based on the state. -
actions
: Functions that can mutate the state and handle business logic (both sync and async).
Note: There’s no need for
mutations
like in Vuex. You can directly modify the state inside actions.
Using the Store in Components
Now that you've created your first Pinia store, let’s see how to use it in Vue components to read and update state.
📥 Step 1: Import and Use the Store
In any Vue component, you can import your store and use it inside the setup()
function (Composition API) or in data()
/methods()
if you're using the Options API.
Here's an example using the Composition API:
<!-- src/components/Counter.vue -->
<template>
<div>
<h2>Counter: {{ counter.count }}</h2>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">Increment</button>
<button @click="counter.decrement">Decrement</button>
<button @click="counter.incrementBy(5)">Increment by 5</button>
</div>
</template>
<script setup>
import { useCounterStore } from '../stores/counter'
const counter = useCounterStore()
</script>
🧠 Explanation
-
useCounterStore()
returns a reactive store instance. -
You can access and modify state directly (
counter.count
). -
Getters like
doubleCount
behave like computed properties. -
Actions are just methods (
counter.increment()
).
✅ Example with Options API
If you're still using the Options API, you can do this instead:
<script>
import { useCounterStore } from '../stores/counter'
export default {
name: 'CounterOptions',
computed: {
counter() {
return useCounterStore()
},
count() {
return this.counter.count
},
double() {
return this.counter.doubleCount
}
},
methods: {
increment() {
this.counter.increment()
}
}
}
</script>
While Pinia is designed to work best with the Composition API, it still supports the Options API for legacy codebases.
Reactivity and DevTools Integration
One of the strongest features of Pinia is its seamless integration with Vue’s reactivity system and the Vue Devtools. This makes debugging and state tracking much easier and more intuitive than ever before.
🧬 Fully Reactive Store
Pinia stores are reactive by default, just like component state. That means:
-
Changes to the state will automatically update the UI.
-
You can use state, getters, and actions anywhere in your app, and they'll remain reactive.
For example:
const counter = useCounterStore()
watch(
() => counter.count,
(newVal) => {
console.log(`Count changed to: ${newVal}`)
}
)
This will respond to changes in counter.count
just like any Vue reactive property.
🔧 Vue Devtools Integration
Pinia integrates smoothly with Vue Devtools, giving you powerful debugging tools:
-
View all defined stores and their current state.
-
See history of mutations/actions via Time Travel Debugging.
-
Trigger or replay actions.
-
Inspect and edit state directly in the devtools.
To Use Devtools:
Make sure you have the latest Vue Devtools extension installed for Chrome or Firefox.
Once running:
-
Open your Vue app in the browser.
-
Open Devtools → Go to the "Pinia" tab.
-
Explore your store state, getters, and action calls.
If Pinia doesn’t appear:
-
Ensure
createPinia()
is installed before mounting the app. -
Make sure you're in development mode (not in a production build).
Modular Stores and Best Practices
As your application grows, having a single monolithic store can quickly become unmanageable. Pinia encourages a modular architecture by design, making it easy to split state management into logical, self-contained stores.
🧩 Creating Multiple Stores
You can organize your application into feature-specific stores. For example:
src/
├── stores/
│ ├── counter.js
│ ├── auth.js
│ └── product.js
Example: auth.js
Store
// src/stores/auth.js
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null,
token: null,
}),
actions: {
login(userData) {
this.user = userData.user
this.token = userData.token
},
logout() {
this.user = null
this.token = null
}
}
})
You can then use it anywhere like this:
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
auth.login({ user: { name: 'Jane' }, token: 'abc123' })
✅ Best Practices
Here are some tips for organizing and scaling your Pinia stores:
-
Group by domain: Keep each store focused on a single domain (e.g.,
auth
,cart
,user
, etc.) -
Avoid global mutations: Use actions to encapsulate logic and ensure clarity.
-
Use TypeScript where possible: Pinia works great with types and offers full auto-completion.
-
Keep state minimal: Don’t store derived values — use getters instead.
-
Lazy-load stores if needed: For large apps with code splitting.
Using Pinia with Composition API and Options API
Pinia is designed to be Composition API-first, but it also supports the Options API to maintain compatibility with existing Vue codebases. Here’s how you can use it in both styles.
🧪 Composition API (Recommended)
When using the Composition API, stores are simply imported and called within the setup()
function (or <script setup>
block), and they’re fully reactive.
Example:
<script setup>
import { useAuthStore } from '../stores/auth'
const auth = useAuthStore()
function logoutUser() {
auth.logout()
}
</script>
You can directly access and modify store state, call actions, and use getters as if they were reactive variables or functions.
✅ Pros:
-
Cleaner and more modular
-
Better TypeScript support
-
Works perfectly with
<script setup>
🧩 Options API
Using Pinia in the Options API is still possible but requires some workarounds, particularly when accessing store state in computed
and store actions in methods
.
Example:
<script>
import { useCounterStore } from '../stores/counter'
export default {
name: 'CounterOptions',
computed: {
counter() {
return useCounterStore()
},
count() {
return this.counter.count
}
},
methods: {
increment() {
this.counter.increment()
}
}
}
</script>
✅ Pros:
-
Maintains legacy compatibility
-
Useful in projects still using the Options API
⚠️ Cons:
-
Slightly more verbose
-
Less TypeScript-friendly
🔍 When to Use Which?
Scenario | Recommended API |
---|---|
New Vue 3 project | Composition API |
Existing Vue 2 or early Vue 3 project | Options API |
TypeScript support needed | Composition API |
Persisting State in Pinia
By default, Pinia stores are in-memory—meaning the state is lost when the page is refreshed. However, you can easily persist your store state using plugins like pinia-plugin-persistedstate
.
This is useful for persisting:
-
Auth tokens
-
User preferences
-
Shopping carts
-
Theme settings
📦 Step 1: Install the Plugin
npm install pinia-plugin-persistedstate
or
yarn add pinia-plugin-persistedstate
⚙️ Step 2: Register the Plugin
In main.js
(or main.ts
):
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.mount('#app')
🗃️ Step 3: Enable Persistence in a Store
Modify a store to enable persistence using the persist
option:
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null,
token: null,
}),
actions: {
login(userData) {
this.user = userData.user
this.token = userData.token
},
logout() {
this.user = null
this.token = null
}
},
persist: true
})
This will store your data in localStorage
by default.
🛠 Custom Storage and Configuration
You can also customize how and where data is stored:
persist: {
key: 'my-auth',
storage: sessionStorage,
paths: ['token'] // Only persist token, not user
}
Now your store’s state will survive page reloads — a must-have for modern web apps.
Testing Pinia Stores
Testing your state management logic is essential for building reliable Vue applications. With Pinia, testing is straightforward because stores are just plain JavaScript modules — no magic, no mocks required (unless you want them).
You can test:
-
Store state directly
-
Actions, including side effects
-
Getters, like computed properties
🧰 Example: Testing a Simple Counter Store
Assume the following counter.js
store:
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})
🧪 Step 1: Set Up a Test File
Create a test file (e.g., counter.spec.js
) using your test framework (Jest, Vitest, etc.):
// tests/stores/counter.spec.js
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../../src/stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should have initial count of 0', () => {
const counter = useCounterStore()
expect(counter.count).toBe(0)
})
it('should increment the count', () => {
const counter = useCounterStore()
counter.increment()
expect(counter.count).toBe(1)
})
it('should return doubleCount correctly', () => {
const counter = useCounterStore()
counter.count = 2
expect(counter.doubleCount).toBe(4)
})
})
🧼 Tips for Testing Pinia Stores
-
Use
setActivePinia(createPinia())
before each test to initialize a fresh store. -
You can directly access and modify the store state.
-
Actions are just methods — no need to mock them unless testing components in isolation.
✅ If you’re testing components that use stores, you can still inject stores normally as part of your test setup (e.g., with
mount()
in Vue Test Utils).
Migrating from Vuex to Pinia
If you have an existing Vue 3 application using Vuex, moving to Pinia is a worthwhile upgrade. Pinia offers a simpler API, full TypeScript support, and better integration with the Composition API.
Fortunately, many Vuex concepts map directly to Pinia — but with much less boilerplate.
🔄 Vuex to Pinia Mapping
Vuex Concept | Pinia Equivalent |
---|---|
state |
state |
getters |
getters |
mutations |
⛔ Not needed |
actions |
actions (combined logic) |
modules |
Separate store files |
mapState/mapGetters |
Direct access or helper composables |
✏️ Example Migration: From Vuex to Pinia
Vuex Store (Old)
// store/index.js
export default {
state: {
count: 0
},
getters: {
doubleCount: (state) => state.count * 2
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => commit('increment'), 1000)
}
}
}
Pinia Store (New)
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
incrementAsync() {
setTimeout(() => this.increment(), 1000)
}
}
})
✅ Much cleaner. No need for a separate mutations
. You update the state directly in actions.
🛠 Migration Tips
-
Modularize your Vuex store: Split your Vuex store into modules if it’s not already — this makes it easier to convert to multiple Pinia stores.
-
Replace Vuex helpers: Replace
mapState
,mapGetters
, etc., with direct store usage (viauseXStore()
). -
Update component logic: Refactor component logic to use Pinia’s Composition API-friendly syntax.
-
Test after each step: Migrate incrementally and verify functionality as you go.
Note: Vuex 5 was in alpha and never finalized — Pinia is now the officially recommended solution.
Conclusion
Pinia has redefined state management in Vue 3 by offering a simpler, cleaner, and more powerful alternative to Vuex. With its intuitive API, tight integration with the Composition API, and excellent TypeScript support, Pinia makes managing application state a much more enjoyable experience — whether you're building a small component or a large-scale SPA.
In this tutorial, you learned how to:
-
Set up and configure Pinia in a Vue 3 project
-
Create and use modular stores with
state
,getters
, andactions
-
Integrate Pinia with components using both the Composition and Options APIs
-
Persist store state using localStorage or sessionStorage
-
Test Pinia stores effectively
-
Migrate from Vuex to Pinia with minimal effort
By embracing Pinia, you're investing in the future of state management in Vue, backed officially by the Vue team. Whether you're building a new project or upgrading an existing one, Pinia is a smart, modern choice to keep your state predictable and maintainable.
You can find the full source code on our GitHub.
===That's just the basics. If you need more deep learning about Vue, you can take the following cheap course:
-
Vuex Vuex with Vue Js Projects to Build Web Application UI
-
Vue 3 and Deno: A Practical Guide
-
Vue 3 Fundamentals Beginners Guide 2023
-
Vue 3, Nuxt. js and NestJS: A Rapid Guide - Advanced
-
Master Vuejs from scratch (incl Vuex, Vue Router)
-
Laravel 11 + Vue 3 + TailwindCSS: Fullstack personal blog.
Thanks!