Pinia is the official state management system for Vue.js, designed to be intuitive, flexible, and TypeScript-friendly.
Pinia was created by Eduardo San Martin Morote, a member of the core Vue.js team, and became the official state management solution for Vue 3 in November 2021.
Previously, Vuex was used as the state management library. But the syntax was more complicated.
Specifically designed for the new features of Vue 3, Pinia leverages all the capabilities this version of the framework offers, with better TypeScript support and a smoother integration with the Composition API.
Installation and Configuration of Pinia
To get started, we need to install Pinia in our project. If you haven’t added it yet, you can do so by running:
npm install pinia
Once installed, we need to configure Pinia in our Vue application. In the main file (main.js
or main.ts
), import and use 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');
Creating a Store in Pinia
In Pinia, a store is a reactive container that holds the state and business logic.
Let’s see how to create a basic store with a simple example that would manage user state.
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
currentUser: null,
}),
actions: {
async fetchUsers() {
const response = await fetch('/api/users');
this.users = await response.json();
},
setCurrentUser(user) {
this.currentUser = user;
},
},
getters: {
activeUsers: (state) => state.users.filter(user => user.isActive),
},
});
In this example, we have,
- State:
users
is an empty array andcurrentUser
isnull
. - Actions:
fetchUsers
fetches users from an API andsetCurrentUser
sets the current user. - Getters:
activeUsers
filters active users.
Parts of a Store
We have just seen how the store is composed of certain parts or “sections” (all optional). These parts are,
Property | Description |
---|---|
state | Defines the initial reactive state of the store |
actions | Contains methods to modify state and handle business logic. |
getters | Computed functions that derive values from the state |
Let’s look at each of them in detail,
State
The state contains the variables and data managed by the store. The state in Pinia is automatically reactive, meaning any changes in the state will be immediately reflected in the components that use it.
userStore.setCurrentUser({ id: 1, name: 'John Doe' });
For example, if we update currentUser
in the store, any component that depends on currentUser
will automatically update:
Actions
Actions handle complex logic or API calls.
actions: {
async fetchUsers() {
try {
const response = await fetch('/api/users');
this.users = await response.json();
} catch (error) {
console.error('Error fetching users:', error);
}
},
},
Getters
Getters are like computed()
but at the store level.
// stores/counter.js
getters: {
multiplyBy: (state) => (factor) => state.count * factor,
}
Like computed properties, getter methods are also cached.
Using the Store in Components
To use the store in a component, we simply import and call the store:
<script setup>
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
// Access the state
console.log(userStore.users);
// Call an action
userStore.fetchUsers();
// Use a getter
console.log(userStore.activeUsers);
</script>
Modifying the State
Logically, we will want to modify the state of the store. In Pinia, we have several ways to do this.
The recommended way to modify the state is through actions:
const counter = useCounterStore()
counter.increment()
In Pinia, we can modify the state directly (without methods or mutations).
counterStore.count++; // ✅ Works (reactivity maintained)
We can also reset the state of a state variable,
counterStore.$reset(); // Goes back to the initial state
To update multiple properties at once, you can use $patch
:
// Passing an object
counter.$patch({
count: counter.count + 1,
name: 'New name'
})
// Passing a function (more efficient for changes in arrays)
counter.$patch((state) => {
state.count++
state.items.push({ id: state.nextId++, text: 'New item' })
})
Reactivity to State
Now we will want our App to react to changes in the store. That is, we will want to maintain reactivity in our components.
Pinia provides a helper called storeToRefs
that automatically converts all properties of the state
and the getters
into reactive refs.
<script setup>
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { users, currentUser, activeUsers } = storeToRefs(userStore);
// Actions are used directly from the store
userStore.fetchUsers();
</script>
users
,currentUser
, andactiveUsers
are nowref()
reactive.fetchUsers
and other actions are kept out ofstoreToRefs
because they are functions and do not need to be reactive.
Destructuring directly from the store would break reactivity. That is, if you do this,
const { users, currentUser } = userStore; // ⚠️ This breaks reactivity
You will break reactivity. That’s why we used storeToRefs
,
Subscriptions to State Changes
Pinia also allows subscribing to changes in the store’s state. Under the hood, it is equivalent to using a watch
.
const unsubscribe = counter.$subscribe((mutation, state) => {
// Executed when the state changes
console.log('Updated state:', state)
console.log('Mutation:', mutation)
// Optionally, you can save the state to localStorage
localStorage.setItem('counter', JSON.stringify(state))
})
// Later you can unsubscribe
unsubscribe()
Using Multiple Stores
In large applications, it is common to split the state into multiple stores. Pinia makes this organization easy by allowing each store to be independent.
For example, we could have one store for users, another for products, and another for application settings.
// stores/
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
}),
});
export const useProductStore = defineStore('product', {
state: () => ({
products: [],
}),
});