Enmilocalfunciona

Thoughts, stories and ideas.

De Vuex a Pinia, la gestión de estados definitiva para Vue

Publicado por Juan Carlos López Muñoz el

FrontEstadosVue.jsVuexPinia

Introducción

Para los proyectos FrontEnd de gran escala, se ha visto la necesidad de usar alguna librería para la gestión del estado (State Management), que sirve como un almacén centralizado para todos los componentes de una aplicación con reglas que garantizan el cambio del estado.

En Vue tenemos disponibles dos librerías oficiales para el manejo de estados. Vuex, la librería oficial en Vue 2 y Pinia, la oficial actual en Vue 3.

En este artículo compararemos ambas librerías y sacaremos conclusiones acerca de su uso, para lo cual utilizaremos Vuex 4 y Pinia 2 sobre un proyecto con Vue 3.

Configuración

Pinia

La instalación de Pinia es sencilla. Para ello escribimos el siguiente comando:

npm install pinia --save  
# O con yarn
yarn add pinia  

Pinia es un contenedor de la Composition API de Vue 3. Por lo tanto, no tiene que inicializar como un complemento, a menos que se desee compatibilidad como por ejemplo con Vue DevTool o SSR.

// main.js

import { createApp } from 'vue';  
import { createPinia } from 'pinia';  
import App from './App.vue';

const app = createApp(App);  
app.use(createPinia());

app.mount('#app');  

Vuex

La instalación de Vuex también es sencilla, pero a diferencia de Pinia, tendremos que importar las stores en la configuración.
Se instala Vuex escribiendo el siguiente comando:

npm install vuex --save  
# O con yarn
yarn add vuex --save  

Una vez instalada la librería, podemos crear una store de prueba y utilizarla de manera global.

//store/countStore.js

import { createStore } from 'vuex'

export const countStore = createStore({  
 state () {
    return {
       count: 0
    }
 },
 mutations: {
   increment (state) {
      state.count++
    }
 }
})
//main.js

import { createApp } from 'vue'  
import { countStore } from './store/countStore'  
import App from './App.vue'

const app = createApp(App);  
app.use(countStore);

app.mount('#app')  

NOTA: Lo normal en un proyecto es crear un módulo global de todas las stores  e importarlo en el main.js.

Uso

El uso de Pinia y Vuex es distinto entre ellos. A continuación, veremos cuales son las diferencias.

Pinia

Para la creación de una store sencilla sería:

//store/useCounterStore.js

import { defineStore } from 'pinia'

export const useCounterStore = defineStore({  
  id: 'countStore',
  state: () => (
    { count: 0, operation: null }
  ),
  getters: {
    lastOperation: (state) => state.operation
  },
  actions: {
    add() {
      this.count++;
      this.operation = 'add';
    },
    remove() {
      this.count--;
      this.operation = 'remove';
    },
    random() {
      this.count = Math.floor(Math.random() * 101);
      this.operation = 'random';
    }
  }
})

Invocamos en la vista o componente que queramos la store y utilizaremos las propiedades deseadas.

//MyComponent.vue

import { useCounterStore } from './store/useCounterStore ';  
import { computed } from 'vue';

export default {  
 setup() {

   // store
   const counterStore = useCounterStore();

   //getters
   const lastOperation = computed(() => counterStore.lastOperation);

   //actions
   const add = () => counterStore.add();
   const remove = () => counterStore.remove();
   const random = () => counterStore.random();
 }
}

Vuex

Para la creación de una store sencilla sería:

//store/countStore.js

import { createStore } from 'vuex'

export const countStore = createStore({  
  state: () => {
      count: 0,
      operation: null,
  },
  getters: {
    lastOperation: (state) => state.operation
  },
  mutations: {
    increment: (state) => {
      state.count++;
      state.operation = 'add'
    },
    decrement: (state) => {
      state.count--;
      state.operation = 'remove'
    },
    random: (state) => {
      state.count = Math.floor(Math.random() * 101);
      state.operation = 'random'
    }
  }
})

Aquí debemos utilizar los mutadores para alterar los estados.

Lo invocamos en una vista o en un componente y para ejecutar las propiedades de la store, usaremos las funcionalidades que nos proporciona Vuex.

//MyComponent.vue

import { useStore } from 'vuex';

export default {  
 setup() {

   // store
   const store = useStore();

   //getters
   const lastOperation = computed(
    () => store.getters['lastOperation']
   );

   //actions
   const add = () => store.commit('add');
   const remove = () => store.commit('remove');
   const random = () => store.commit('random');
 }
}

Acciones asíncronas

En este punto vamos a analizar cómo actuarían las acciones asíncronas en cada librería.

Pinia

Con Pinia simplemente creamos una función asíncrona, donde cambiaremos los estados en la propia función.

//store/useCounterStore.js

export const useCounterStore = defineStore({  
  //...

  actions: {
    async random() {
      try {
        this.count = await randomApiFecth();
        this.operation = 'random';
      } catch(error) {
        this.error.push(error);
      }
    }
  }
})

Vuex

En cambio Vuex necesitará el uso de las mutaciones para alterar el estado por estas razones:

  1. En las mutaciones puedes cambiar el estado. Las acciones no podrán.
  2. Las acciones soporta código asíncrono, pero no en las mutaciones.
//store/countStore.js

export const countStore = createStore({  
  state () => {
      count: 0,
      operation: null,
      error: [],
  },
  actions: {
    async random({ commit }){
      try{
        const number = await randomApiFecth();
        commit('random', number);
      } catch(error) {
        commit('error', error);
      }

    },
  },
  mutations: {
    random: (state, number) => {
      state.count = number;
      state.operation = 'random'
    },
    error: (state,error) => {
      state.error.push(error);
    }
  }
})

TypeScript

Pinia

Migrar de JavaScript a TypeScript en Pinia es súper sencillo, ya que Pinia está escrito en TS, lo que nos permite trabajar cómodamente con él sin tener que configurar nada extra, simplemente habría que tiparlo.

//store/useCounterStore.ts

export interface CountState {  
  count: number,
  operation: string | null,
  error: Array<String>,
}

export const useCounterStore = defineStore({  
  id: 'countStore',
  state: () => (
    { count: 0,
      operation: '',
      error:[],
    } as CountState 
  ),
  getters: {
    lastOperation: (state): string | null  => state.operation
  },
  actions: {
    async random(): Promise<number | void> {
     //...
    }
  }})

Vuex

En cambio, en Vuex, si estamos utilizando la Composition API, tenemos que aplicar una serie de pasos:

1. En la Store definimos la clave del tipo InjectionKey.

//store/countStore.js

export interface CountState {  
  count: number,
  operation: string | null,
  error: Array<String>,
}

// Define injection key
export const key: InjectionKey<Store<CountState>> = Symbol()  
export const countStore = createStore<CountState>({  
//...
})

2. Pasamos la InjectionKey en la configuración del proyecto.

//main.js

import { createApp } from 'vue'  
import { countStore, key } from './store/countStore'  
import App from './App.vue'

const app = createApp(App);

// Pass the injection key
app.use(countStore, key);

app.mount('#app')  

3. Pasamos la clave en el método useStore para recuperar la store tipada.

//MyComponent.vue

import { useStore } from "vuex";  
import { key } from './store/countStore';

export default {  
 setup() {
   //...

   // store
   const store = useStore(key);
 }
}

Nos podemos ahorrar el paso 2 creando una función propia para recuperar la store tipada.

//store/countStore.js

import { InjectionKey } from 'vue';  
import { createStore, useStore as baseUseStore, Store } from 'vuex'

export interface CounterState {.....}

// Define injection key
export const key: InjectionKey<Store<CounterState>> = Symbol();

// Define your own `useStore` composition function
export function useStore () {  
  return baseUseStore(key)
}

Y en el componente importamos la función creada desde la propia store.

//MyComponent.vue

import { useStore } from './store/countStore';

export default {  
 setup() {
   //...

   // store
   const store = useStore();
 }
}

Módulos

Pinia

En Pinia no hay necesidad de registrar módulos, ya que las stores son dinámicas por el diseño y solo se activan cuando se necesiten. Se podría decir que de serie son módulos.

//store/useCounterStore.js

export interface CountState {  
  count: number,
  error: Array<String>,
}

export const useCounterStore = defineStore({  
  id: 'countStore',
  state: () => (
    //...
  ),
  actions: {
    add(){
     //...
    },
    remove(){
     //...
    },
    async random(): Promise<number | void> {
     //...
    }
});


//store/useOperationStore.js

export interface OperationState {  
  operation: string | null,
}

export const useOperationStore = defineStore({  
  id: 'operationStore',
  state: () => (
    {
      operation: null,
    } as OperationState
  ),
  getters: {
    lastOperation: (state: OperationState): string | null  => state.operation
  },
  actions: {
    setOperation(name: string){
      this.operation = name;
    },
  }
});

Vuex

Con Vuex si tenemos que configurarlo a través de módulos por su uso de un solo árbol de estados, es decir todos los estados de la aplicación están contenidos dentro de un gran objeto. Sin embargo, a medida que nuestra aplicación crece, la gestión de las stores puede complicarse.
Para ello, definimos todos los módulos en una store principal.

//store/index.js

import Vuex from 'vuex';

export default new Vuex.Store({  
  state: {},
  mutations: {},
  actions: {},
  modules: {
   count: countStore,
   operation: operationStore,
  }
})

Dentro de cada store, añadimos la propiedad namespaced para poder diferenciar todas sus propiedades.

//store/countStore.js

import { createStore } from 'vuex'

export const countStore = createStore({  
    namespaced: true,
    state: { //... },
    mutations: { //... }
})

De esta forma, se asignan automáticamente a una ruta en función del nombre con el que se registró el módulo.

//MyComponent.vue

export default {  
 setup() {
   //...

   //actions
   const add = () => store.dispatch('count/add');
   const remove = () => store.commit('count/remove');
   const random = () => store.dispatch('count/random');
 }
}

Depuración

Con la extensión del navegador Vue DevTools para depurar aplicaciones de Vue no notamos gran diferencia entre ambas librerías, aunque hasta hace poco Pinia no era compatible con Vue DevTools en algunas características como el TimeLine o poder editar el valor de los estados.
Como detalle, con Pinia nos muestran en la consola las stores instaladas.

Rendimiento

Tanto Pinia como Vuex son rápidos y, en algunos casos, con Pinia será más rápido en comparación con Vuex. Este rendimiento se puede atribuir al peso extremadamente ligero de Pinia (alrededor de 1 KB).

Comunidad

Algunos puntos que afectan al uso y aceptación de ambas librerías por parte de la comunidad de desarrolladores son los siguientes:

  • Aprendizaje: La curva de aprendizaje para ambas librerías es fácil para desarrolladores con experiencia previa con librerías de arquitectura Flux como Redux, pero en mi opinión personal, para los desarrolladores que se embarcan sin experiencia, veo más intuitivo Pinia por su facilidad de manejo.
  • Documentación: La documentación de ambas librerías es muy buena y ambas están escritas de manera que lo entiendan tanto los desarrolladores experimentados como los novatos.
  • Recursos: Para ambas librerías encontramos bastante información en blogs, en YouTube e incluso en StackOverflow para solucionar los problemas. Aunque es cierto, que para Vuex obtenemos más información por tener un recorrido más largo.
  • Calificación en GitHub: Al momento de hoy,  Pinia ha lanzado dos versiones con las que obtiene más de  5,5K estrellas, algo que no está nada mal, mientras que las 4 versiones estables de Vuex han recibido más de 27K estrellas.

Conclusión

Podemos concluir que Pinia sería una buena opción para los proyectos de Vue, por su gran simplicidad en el código y accesibilidad, sobre todo si estamos en un proyecto con TypeScript. Sin embargo, en algunos de los aspectos son muy similares como puede ser el rendimiento que generan ambas.

Quizás por todas estas razones, el equipo de Vue adoptó esta librería como oficial y recomiendan su uso para proyectos Vue 3, sustituyendo a Vuex como opción por defecto, aunque Vuex 4 sigue siendo una opción viable.

¿Te ha gustado el post? Síguenos en Twitter para estar al tanto de los próximos.