Tabla de contenidos
Es un ‘State Management Pattern’ basado en el patrón Flux que sirve para controlar el flujo de datos en una aplicación. Sustituye a la anterior librería usada en Vue 2 llamada Vuex.
Según la filosofía de Vue cada componente es una unidad funcional que contiene 3 partes:
Por ejemplo, el componente contador sería:
<script setup>
import { ref } from 'vue'
// state
const count = ref(0)
// actions
const increment = () => {
count.value++
}
</script>
<!-- view -->
<template>
{ { count }}
<button @click="increment">Increment</button>
</template>
Esto es lo que se llama one-way data flow.
El problema lo tenemos cuando un componente necesita acceder a datos (state) de otro componente.
En Vue la comunicación entre componentes padre-hijo se hace hacia abajo mediante props y hacia arriba emitiendo eventos (emit). Y vimos que si distintos componentes que no son padre-hijo tenían que compartir un mismo estado (acceder a los mismos datos) surgían problemas e intentamos solucionarlos con el patrón store pattern. Esto puede servir para pequeñas aplicaciones pero cuando crecen se hace difícil seguir los cambios. Para esos casos debemos usar Pinia, que proporciona un almacén de datos centralizado para todos los componentes de la aplicación y asegura que los datos sólo puedan cambiarse de forma controlada.
El uso de Pinia es imprescindible en aplicaciones de tamaño medio o grande pero incluso para aplicaciones pequeñas nos ofrece ventajas frente a un store pattern hecho por nosotros como soporte para las DevTools y para Server Side Rendering o uso de Typescript.
Como ya dijimos, no debemos almacenar todos los datos en el store centralizado sino sólo los que necesitan varios componentes (los datos privados de un componente deben permanecer en él).
La forma más sencilla de utilizar Pinia es incluirla a la hora de crear nuestro proyecto cuando nos pregunta si queremos usarla. Esto hace que la instalación y configuración de la herramienta se haga automáticamente.
Al entrar en nuestro nuevo proyecto vemos que dentro de /src se ha creado una carpeta llamada stores/ donde crearemos los distintos almacenes de datos (podemos tener sólo uno o varios).
Para poder usar Pinia en los distintos componentes vemos que en el fichero main.js se importa la función createPinia() y se indica que se use en la instancia de Vue:
import { createApp } from 'vue'
import { createPinia } from 'pinia' // <---
import App from './App.vue'
import router from './router'
createApp(App).use(createPinia()).use(router).mount('#app')
Si queremos usar Pinia en un proyecto existente donde no la seleccionamos al crear el proyecto deberemos instalar la librería como dependencia de producción y modificar el fichero main.js para que pueda usarse, como hemos visto arriba. Luego crearemos la carpeta /src/stores/ y en ella los almacenes que queramos usar.
Ahora hay que crear el fichero del store. Podemos tener todos los datos en un único fichero o, si son muchos, hacer ficheros diferentes. Por ejemplo para la aplicación de ‘ToDo’ podemos crear su store en /src/stores/toDo.js.
Al crear un almacén pondremos en él todas las variables que vaya a usar más de un componente y los métodos para acceder a ellas y modificarlas. Pr ejemplo, para compartir un contador haríamos:
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const binaryCount = computed(() => count.value.toString(2))
function increment() {
count.value++
}
function decrement() {
count.value--
}
return { count, binaryCount, increment, decrement }
})
En este ejemplo hemos creado un almacén que tiene un dato (count), un dato calculado (binaryCount) que es el contador en binario y dos métodos para cambiar su valor (increment y decrement). El primer parámetro de defineStore es el nombre con el que veremos el almacén desde las DevTools (por si tenemos varios) y el segundo su función setup.
Desde la consola del navegador podemos usar las DevTools para ver nuestro almacén. Para ello vamos a la pestaña de Vue y desde el Inspector buscamos Pinia:

Si al crear el proyecto hemos incorporado Pinia nos ha creado un almacén de ejemplo como el anterior.
El store puede acceder a variables globales como router o route si las necesita importándolas directamente:
import { useRouter, useRoute } from 'vue-router'
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const router = useRouter()
const route = useRoute()
...
})
También puede importar otros stores si necesita acceder a sus datos.
Para usar el almacén en un componente debemos importarlo y crear una instancia del mismo. Esto se hace llamando a la función que hemos exportado al definir el almacén (en este caso useCounterStore()). Con eso ya podemos usar el almacén como si fuera local al componente. Por ejemplo, en un componente Counter.vue haríamos:
<script setup>
import { useCounterStore } from '../stores/counterStore.js'
const counterStore = useCounterStore()
</script>
<template>
<div>
<p>Count: { { counterStore.count }}</p>
<p>Binary Count: { { counterStore.binaryCount }}</p>
<button @click="counterStore.decrement">-</button>
<button @click="counterStore.increment">+</button>
</div>
</template>
Si no queremos acceder a todo el almacén sino sólo a algunas variables podemos usar la desestructuración pero no directamente (se perdería la reactividad) sino con storeToRefs():
<script setup>
import { useCounterStore } from '../stores/counterStore.js'
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
const { count } = storeToRefs(counterStore)
const { increment } = counterStore
</script>
Con las acciones no es necesario usar storeToRefs() porque no son reactivas.
Para crear un almacén en Pinia, lo definimos igual que antes pero el segundo parámetro será un objeto con 3 propiedades:
state: el estado inicial del almacén, es decir, las variables que contendrá.getters: funciones que permiten obtener información derivada del estado (computed).actions: funciones que permiten modificar el estado.import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
binaryCount: (state) => state.count.toString(2)
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
})
Cada componente que necesite acceder al almacén de datos lo importa y:
mapStatemapActionsEn los helpers mapState y mapActions indicaremos las variables y métodos del store que queremos usar en este componente. Su sintaxis, como pasaba con los props, puede ser en forma de array o en forma de objeto (si queremos personalizar el nombre de la variable o método):
//MyComponent.vue
import { useCounterStore } from '../stores/conterStore';
import { mapState, mapActions } from 'pinia';
export default {
...
computed: {
...mapState(useCounterStore, ['count', 'binaryCount'])
},
methods: {
...mapActions(useCounterStore, ['increment', 'decrement'])
}
}
Con esto se mapean las variables, getters y actions a variables y métodos locales a los que podemos acceder desde this. (por ejemplo this.count o this.increment()).
En forma de objeto sería:
computed: {
...mapState(useCounterStore, {
countInStore: 'count',
}),
...mapActions(useCounterStore, {
up: 'increment',
down: 'decrement',
})
}
y se llamarían con **this.countInStore o this.up(3).
Aquí definiremos variables calculadas (por ejemplo sólo las tareas pendientes del array todos sino) haciendo un método que nos devuelva directamente las tareas filtradas. Estos getters funcionan como las variables computed (sólo se ejecutan de nuevo si cambian los datos de que dependen):
import { defineStore } from 'pinia'
export const useToDoStore = defineStore('todo', {
state: () => ({
/** @type { { title: string, id: number, done: boolean }[]} */
todos: [
{ id: 1, title: '...', done: true },
{ id: 2, title: '...', done: false }
],
nextId: 3,
}),
getters: {
// reciben como primer parámetro el 'state'
finishedTodos: (state) => state.todos.filter((todo) => todo.done),
unfinishedTodos: (state) => state.todos.filter((todo) => !todo.done),
/**
* @returns { { title: string, id: number, done: boolean }[]}
*/
},
actions: {
// any amount of arguments, return a promise or not
addTodo(title) {
this.todos.push({
title,
id: this.nextId,
done: false
})
this.nextId++
},
},
})
Cada getter recibe como primer parámetro el state del almacén.
Dentro de los componentes se usan como cualquier variable del state:
export default {
...
computed: {
...mapState(useToDoStore, {
todos: 'todos',
finishedTodos: 'finishedTodos',
})
},
}
Los getters pueden recibir parámetros, por ejemplo, para hacer búsquedas:
getters: {
getTodoById: (state) => (id) => state.todos.find((todo) => todo.id === id)
}
Desde el componente lo llamaremos con this.getTodoById(2).
La manera de cambiar los datos del almacén es llamando a las acciones que hayamos definido, y que hemos mapeado al componente como métodos locales. Estas acciones pueden recibir tantos parámetros como se desee.
Cada vez que se llama a una acción se registra en las DevTools y podemos ver la acción llamada y los datos que se le han pasado:

Las acciones pueden hacer llamadas asíncronas. Lo normal es llamar a la BBDD y cuando el servidor responda modificaremos los datos del store.
import { defineStore } from 'pinia'
import TodoService from '../services/TodoService.js'
export const useToDoStore = defineStore('todo', {
state: () => {
return {
todos: [],
nextId: 0,
}
},
actions: {
async addTodo(title) {
try {
const newToDo = await TodoService.addTodo({
title,
id: this.nextId + 1,
isFinished: false
});
this.nextId++
this.todos.push(newToDo)
} catch(error) {
throw error;
}
},
},
})
Si la acción realiza una llamada asíncrona y el componente que la llama tiene que enterarse de cuándo finaliza debe devolver una promesa (debe declararse con async o envolverse en un return new Promise(...)). En el componente podemos usar await o then / catch para saber cuándo ha acabado la acción:
try {
await this.addTodo(this.newTodo)
alert('Añadida la tarea ' + this.newTodo.title)
this.$router.push('/todos')
} catch(error) {
alert(error)
}
NOTA: si quien llama a una acción no necesita saber cuándo termina la acción ni su resultado no es necesario llamarla con await.
Aunque no es lo habitual, si queremos usar un formulario para modificar un state del store no podemos asociarlo al input con la directiva v-model porque cuando el usuario cambie el valor del input estaría escribiendo directamente sobre un state, lo que debe hacerse por medio de una acción.
Tenemos 2 soluciones al problema:
Podéis obtener más información sobre Pinia en la documentación oficial.