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 Vue.js core team, and became the official state management solution for Vue 3 in November 2021.
Vuex was previously 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 offered by this version of the framework, with better TypeScript support and smoother integration with the Composition API.
Pinia Installation and Configuration
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 must 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:
usersis an empty array andcurrentUserisnull. - Actions:
fetchUsersfetches users from an API andsetCurrentUsersets the current user. - Getters:
activeUsersfilters active users.
Parts of a store
We just saw 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 the 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 change in the state will immediately be 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 update automatically:
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
Naturally, we will want to modify the store’s state. 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 the need for methods or mutations).
counterStore.count++; // ✅ Works (reactivity maintained)
We can also reset a state variable’s state,
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 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, andactiveUsersare now reactiveref()s.fetchUsersand other actions remain outside ofstoreToRefsbecause they are functions and don’t need to be reactive.
Destructuring the store directly would cause reactivity to be lost. That is, if you do this,
const { users, currentUser } = userStore; // ⚠️ This breaks reactivity
You will break reactivity. That’s why we used storeToRefs,
Subscribing to state changes
Pinia also allows subscribing to changes in the store’s state. Under the hood, it’s the equivalent of 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’s common to split the state into multiple stores. Pinia facilitates this organization by allowing each store to be independent.
For example, we could have a 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: [],
}),
});
