Manejo de estado centralizado en aplicaciones Vue.js con Vuex
Precaución
Este artículo está dirigido a Vue 2. Vue 3 ya está disponible y es la forma recomendada de desarrollar aplicaciones con Vue.
Entre las soluciones comúnmente usadas para enviar y recibir información entre páginas se encuentra el envío de datos a través del URL, las cookies y el LocalStorage. Todas éstas formas si bien son funcionales no proveen de un estándar para la manipulación de los datos y no son fuentes de datos reactivas que hagan armonía con la reactividad de Vue.js.
Evan You, creador de Vue.js, desarrolló una herramienta llamada Vuex especialmente diseñada para Vue que permite el manejo centralizado de datos en aplicación web desarrolladas con este framework. Si bien no es la única herramienta para el manejo de datos, es la que mejor se integra con el ecosistema de Vue.
Vuex es una librería de manejo de patrones de estado para aplicaciones Vue.js. Provee un almacenamiento centralizado para todos los componentes en la aplicación, con reglas que aseguran que los datos sólo puedan ser modificados de forma predecible.
Usar Vuex es especialmente recomendado para aplicaciones medianas y grandes donde se requiere información centralizada y reactiva accesible a través de distintos componentes.
Instalación
A través de un CDN, colocándolo después de vue
<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>
Utilizando NPM
npm install vuex --save
Utilizando Yarn
yarn add vuex
Al usar un sistema de módulos (como webpack en el vue-cli), es necesario instalar Vuex a través de Vue.use()
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
Estructura básica de Vuex
const store = new Vuex.Store({
state: {},
mutations: {},
actions: {},
getters: {},
});
state
El state
es el objeto que contiene todos los datos que podemos acceder desde los distintos componentes. Su contenido sólo debe ser modificado a través de las mutations
de tal forma que la modificación del mismo sea transparente y auditable.
state: {
contador: 0;
}
mutations
La única forma de modificar el state
de un almacenamiento Vuex es a través de una mutation
. En estas funciones es que se realizan las modificaciones y las mismas reciben el state
como primer parámetro.
mutations: {
incrementar (state) {
state.contador++
}
Las mutaciones no se pueden llamar directamente, por lo que son ejecutadas con store.commit
store.commit('incrementar');
Las mutaciones también pueden recibir datos como segundo parámetro, esto puede ser un número, una cadena, un arreglo, etc...
mutations: {
incrementar (state, cantidad) {
state.contador += cantidad
}
store.commit('incrementar', 10);
Un punto importante es saber que las mutaciones son síncronas, es decir cualquier modificación al state
debe realizar de una vez y no a través de transacciones asíncronas como consultas a bases de datos o APIs. Para modificaciones asíncronas al state
se utilizan los actions
.
actions
Los actions
son similares a las mutaciones excepto por dos diferencias:
- Se ejecutan con
store.dispatch
- En vez de modificar el
state
, losactions
realizan mutaciones. - Los
actions
pueden contener código asíncrono. - Reciben como primer parámetro una variable
context
que nos da acceso alstate
y a lasmutations
,actions
ygetters
. - Los
actions
pueden retornar una promesa una vez son resueltos.
Suponiendo que quisiéramos incrementar el contador después de una consulta a algun API podríamos hacerlo de la siguiente forma
actions: {
incrementoAsincrono (context) {
return new Promise((resolve, reject) => {
fetch('algunApiX').then(() => {
context.commit('incrementar')
resolve()
})
})
}
}
o utilizando async/await
actions: {
async incrementoAsincrono (context) {
await fetch('algunApiX')
context.commit('incrementar')
}
}
y utilizarlo de la siguiente forma
store
.dispatch('incrementoAsincrono')
.then(() => console.log('¡Contador incrementado!'));
Conoce más sobre Promesas de Javascript.
getters
Los getters
son utilizados para traer información del state
de forma procesada. Estos reciben el state
como primer argumento.
Suponiendo que en el state
tuviéramos un listado de tareas, podríamos crear un getter que retorne solamente las tareas completadas.
state: {
tareas: [
{ id: 1, texto: 'lorem ipsum', realizada: true },
{ id: 2, texto: 'lorem ipsum', realizada: true },
{ id: 3, texto: 'lorem ipsum', realizada: false },
{ id: 4, texto: 'lorem ipsum', realizada: false },
{ id: 5, texto: 'lorem ipsum', realizada: true },
],
},
getters: {
tareasRealizadas (state) {
return state.tareas.filter(tarea => tarea.realizada)
}
}
y usarlo de esta forma
store.getters.tareasRealizadas;
/*
[
{ id: 1, texto: 'lorem ipsum', realizada: true },
{ id: 2, texto: 'lorem ipsum', realizada: true },
{ id: 5, texto: 'lorem ipsum', realizada: true },
]
*/
Los getters
también pueden recibir datos retornando una función. Es decir que podríamos traer una tarea específica por id
a través de un getter de la siguiente forma
getters: {
tareasRealizadasPorId (state) => (id) => {
return state.tareas.filter(tarea => tarea.id === id)
}
}
Y usarlo así
store.getters.tareasRealizadasPorId(2); // [{ id: 2, texto: 'lorem ipsum', realizada: true }]
De esta forma podemos manejar información accesible en nuestros componentes la cual es manipulable a través de los mutations
y los actions
y la cual podemos consultar de forma procesadas a través de los getters
. Dándole a nuestra aplicación un patrón de uso de datos estándar que nos permite escalar más rápidamente.