vuejs-gestion-estado-con-pinia

What is Pinia and how to use the state manager in Vue.js

  • 5 min

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 and currentUser is null.
  • Actions: fetchUsers fetches users from an API and setCurrentUser 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,

PropertyDescription
stateDefines the initial reactive state of the store
actionsContains methods to modify state and handle business logic.
gettersComputed 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, and activeUsers are now ref() reactive.
  • fetchUsers and other actions are kept out of storeToRefs 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: [],
  }),
});