Estado Centralizado con Vue y Vuex / Pinia
El manejo de estado de forma centralizada (también llamado fuente de la verdad ) es algo de lo que disponen la mayoría de frameworks de frontend modernos.
En el ecosistema de Vue.js los más populares son Vuex y Pinia, mientras que por la parte de React, el más popular es Redux.
Aunque puedan parecer un poco confusos y pesados al principio, en este articulo lo vamos a explicar de forma sencilla para que tú también puedas empezar a usarlo en tus proyectos.
⚠️ ¿Qué problema queremos solucionar?
Normalmente en Vue el paso de información entre componentes se hace de dos maneras:
- En caso de querer pasar un dato hacia un componente hijo: se lo pasará el componente que lo invoca y se lo pasará a través de una propiedad.
- En caso de querer pasar un dato hacia un componente padre: ese componente emitirá un evento que será recogido por el componente padre.
Ahora bien, si queremos pasar un dato a un componente hermano, ese componente tendrá que emitir un evento y luego el padre tendrá que pasar esa información como propiedad hacia el componente destinatario.
En el siguiente diagrama se muestra en rojo el recorrido por el que tendría que pasar nuestro dato en caso de querer pasar desde el componente C hasta el componente ABZ.
Como podemos ver, conforme nuestras aplicaciones crezcan en tamaño y en complejidad, este sistema se puede llegar a complicar bastante, llegando incluso a que nuestro código quede mal estructurado y sea poco mantenible en el tiempo. Pasando a convertirse en lo que conocemos como código spaghetti.
Piensa en cómo gestionarías los datos para controlar cosas como: información sobre el layout, el idioma de la aplicación, el modo dark o la información del usuario conectado, que se usarán a lo largo de la aplicación.
¿Complicado verdad? Aquí es donde nos ayuda tener un estado centralizado.
🎓 Flux - el inicio
Como habíamos introducido antes, una de las librerías más conocidas de manejo de estado es Redux, y aunque se suele usar junto a React, realmente es agnóstico al framework que quieras usar.
El motivo por el cual Redux y Vuex son muy similares es que ambos se basan en el patrón Flux, un patrón de diseño basado en tener una fuente de la verdad y un flujo de información en una sola dirección. Para conocer más detalles sobre este patrón podemos encontrar más información en este enlace.
Aquí tenemos un esquema visual que resume como funciona el patrón Flux:
No te preocupes si no lo entiendes, explicaremos los conceptos en detalle más adelante.
🛠️ Sin usar librerías externas
Muchas veces tenemos claro que el alcance de nuestra aplicación no será muy extenso, pero aun así nos vendría bien tener un sistema para gestionar información de forma centralizada sin llegar a tener que integrar Vuex o Redux.
Aunque esto no suele ser recomendable hay casos donde tiene valor.
Para esos casos, tanto React como Vue 3 lo tienen muy sencillo para implementar este patrón, utilizando sus respectivos sistemas de reactividad para compartir un objeto reactivo entre todos los componentes que lo vayan a usar.
En el caso de React también tenemos disponible la Context API, que es similar a como funciona Provide/Inject en Vue. Veamos un ejemplo:
// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we're passing "dark" as the current value.
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
// Assign a contextType to read the current theme context.
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is "dark".
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
En el caso de Vue, haciendo uso de su nuevo sistema de reactividad desacoplado del framework, podemos implementarlo así:
<script>
// store.js
import { reactive } from 'vue'
export const store = reactive({
count: 0
})
</script>
<!-- ComponentA.vue -->
<script>
import { store } from './store.js'
export default {
data() {
return {
store
}
}
}
</script>
<template>From A: {{ store.count }}</template>
<!---------------------->
<!-- ComponentB.vue -->
<script>
import { store } from './store.js'
export default {
data() {
return {
store
}
}
}
</script>
<template>From B: {{ store.count }}</template>
<!---------------------->
Aunque ambos casos funcionan, deberían ser utilizados solo en casos muy concretos donde tenemos muy claro el alcance y tenemos controlados todos los lugares donde se va a utilizar ese "store".
📦 Vuex - el presente
Vuex es el equivalente a Redux, pero en el ecosistema Vue, de forma similar, sirve para crear y gestionar una fuente de la verdad, con el añadido de que se integra mucho mejor con Vue ya que se creó pensando en eso.
Para empezar a usar Vuex primero necesitamos entender algunos conceptos:
-
State
Este es el objeto que define la estructura de los datos que vamos a querer gestionar, muy similar a la propiedad
data
de un componente Vue. -
Mutation
Se trata de funciones a las que se les pueden pasar parámetros y que se encargan de modificar directamente el estado.
Importante: deben ser funciones síncronas
-
Action
Similar a las mutation, las action se encargan de ejecutar los procesos que requieran de acciones asíncronas, como por ejemplo ejecutar alguna llamada a una API o copiar algo al portapapeles.
Hay que tener en cuenta que para modificar el estado, se deben usar las mutaciones.
-
Getter
Se trata de una propiedad que se calcula en base a una o más propiedades del store, similar a como funcionan los
computed
en Vue, con la diferencia de que estos están enfocados al uso del state. -
Module
El concepto de módulo consiste en separar nuestro estado centralizado de vuex en diferentes partes especializadas, a las que luego accederemos por el namespace de cada uno.
Cada módulo puede llegar a contener todo lo nombrado anteriormente: state, mutations, getters y actions.
En este esquema extraído de la propia documentación oficial de Vuex te lo explica de forma más visual
Actualmente ya no se recomienda el uso de Vuex para proyectos nuevos debido a que se encuentra en estado de mantenimiento y no recibirá nuevas funcionalidades.
Puedes encontrar más detalles en este enlace.
🍍 Pinia - el futuro
Como explican en su página, Pinia comenzó como un experimento aparte de Vuex en Noviembre de 2019, donde se pretendía experimentar con el concepto de store, además de integrarlo con la Composition API de Vue 3.
Conforme el proyecto fue avanzando fue ganando funcionalidades, hasta el punto actual donde ha reemplazado a Vuex como librería oficial.
A nivel conceptual Pinia es muy similar a Vuex, las principales diferencias son estas:
-
Eliminación de mutaciones: Se elimina este concepto en pos de simplificar el flujo de datos y eliminar un concepto que se usaba por cuestiones técnicas (principalmente para debug).
-
Soporte para Typescript: Como casi todo el ecosistema de Vue 3, Pinia ya viene preparado para su integración con Typescript. Algo que en Vuex era deficiente al no ser posible saber la información de los tipos al invocar a las mutaciones y acciones por el nombre de la función pasado como string.
-
Eliminación de módulos: Este concepto deja de existir. Se utilizaban en Vuex para la separación de funciones por namespace, pero Pinia lo plantea de otra forma, ya que platea un sistema más horizontal, donde podemos crear distintos store y comunicarlos individualmente sin un marco que los envuelva a todos.
Veamos un ejemplo de cómo funciona esto:
import { defineStore } from 'pinia'
export const todos = defineStore('todos', {
state: () => ({
/** @type {{ text: string, id: number, isFinished: boolean }[]} */
todos: [],
/** @type {'all' | 'finished' | 'unfinished'} */
filter: 'all',
// type will be automatically inferred to number
nextId: 0,
}),
getters: {
finishedTodos(state) {
// autocompletion! ✨
return state.todos.filter((todo) => todo.isFinished)
},
unfinishedTodos(state) {
return state.todos.filter((todo) => !todo.isFinished)
},
/**
* @returns {{ text: string, id: number, isFinished: boolean }[]}
*/
filteredTodos(state) {
if (this.filter === 'finished') {
// call other getters with autocompletion ✨
return this.finishedTodos
} else if (this.filter === 'unfinished') {
return this.unfinishedTodos
}
return this.todos
},
},
actions: {
// any amount of arguments, return a promise or not
addTodo(text) {
// you can directly mutate the state
this.todos.push({ text, id: this.nextId++, isFinished: false })
},
},
})
A primera vista podemos ver que es bastante similar a como funciona Vuex.
Vamos por partes:
-
Lo primero, en la línea 1, se importa la función
defineStore
de Pinia. De aquí parte todo. -
En la línea 3 vemos que el primer argumento que pasamos a ese método es un string identificativo de nuestro store. El segundo argumento es muy parecido al objeto que creamos en Vuex.
-
Como hemos comentado antes, Pinia está pensado desde el principio para integrarse con Typescript. En este ejemplo, aunque estamos usando Javascript, estamos agregando la información de los tipos a través de comentarios JSDoc.
En caso de estar utilizando un editor de código compatible con este formato, lo aprovechará para mostrarnos ayuda basada en los tipos, como autocompletado al escribir o advertencias sobre los tipos.
Un ejemplo de dónde ocurriría esto es la línea 15, en la cual al escribir
state.
, nos sugerirátodos
y sabrá que es un array.
¡Y eso es todo, estos son los conceptos básicos para que puedas empezar a usar Vuex o Pinia en tus proyectos!
Créditos
Foto por Marcin Jozwiak en Unsplash.