wandres.dev
REDUX FUERA DE REACT · agnóstico de vista

Pinia y Vuex en Vue: la reactividad como respuesta

Vue llegó al mismo problema que Flux y lo resolvió dos veces. Vuex tradujo la ceremonia de Redux al vocabulario de Vue con mutations, actions y getters; Pinia la desmontó al descubrir que el sistema de reactividad del framework ya garantizaba gratis casi todo lo que aquella ceremonia protegía. Esta lección compara las dos generaciones, explica por qué Pinia pudo borrar las mutations sin perder trazabilidad, muestra qué principio del patrón se conserva intacto y cuál se negocia, y extrae la enseñanza incómoda para quien viene de Redux: parte de la disciplina que creías esencial era compensación por una carencia del entorno, no una verdad del patrón.

⏱ 18 min

Vue se topó con el mismo problema que llevó a Facebook a inventar Flux —muchos componentes tocando un estado compartido, cambios imposibles de rastrear— y lo resolvió dos veces, con diez años de distancia y con conclusiones opuestas sobre cuánta ceremonia hace falta. La primera respuesta, Vuex, fue una traducción casi literal de Redux al idioma de Vue: un store único, mutations que hacían el papel de reducers y actions que hacían el de thunks. La segunda, Pinia, es hoy la oficial y llegó a una conclusión que sacude las certezas de este track: eliminó las mutations por completo sin perder por ello la trazabilidad ni el orden. Entender por qué pudo hacerlo, y qué tuvo que dar el framework a cambio, es una de las lecciones más útiles del nivel.

🎯 Al terminar esta lección sabrás
  • Reconocer en Vuex la traducción directa de Flux al vocabulario de Vue, pieza por pieza.
  • Entender qué garantiza el sistema de reactividad de Vue que en React había que construir a mano.
  • Explicar por qué Pinia pudo suprimir las mutations sin sacrificar el rastro de los cambios.
  • Distinguir qué parte del patrón es invariante y qué parte era compensación del entorno.

Vuex: Flux con acento de Vue

Vuex nació de la misma observación que Redux y adoptó su forma con fidelidad notable. Un único store por aplicación, estado que los componentes leen pero no escriben, y dos caminos separados para cambiarlo: mutations, funciones síncronas que son las únicas autorizadas a tocar el estado, y actions, que pueden ser asíncronas y cuyo trabajo final es hacer commit de una mutación. La correspondencia con lo que ya sabes es exacta.

🗄️

state → estado

Un objeto reactivo único. Igual que el árbol de Redux, salvo que aquí es mutable y el framework observa las escrituras.

✏️

mutations → reducers

Únicas autorizadas a escribir. Síncronas por obligación, precisamente para que el historial de DevTools tenga sentido.

🌐

actions → thunks

La frontera impura: peticiones, temporizadores, orquestación. Terminan haciendo commit de una o varias mutaciones.

🧮

getters → selectores

Estado derivado y memoizado por el sistema de reactividad. El equivalente natural de createSelector.

La separación entre las dos rutas de escritura tenía una justificación concreta que hoy se olvida: las mutaciones eran síncronas por obligación para que el historial de la extensión de desarrollo pudiera asociar cada cambio de estado con la instrucción exacta que lo produjo. Si una mutación pudiera esperar a la red, el registro mostraría cambios sin causa visible y el viaje en el tiempo dejaría de reconstruir estados válidos. Aquí está el mismo razonamiento que separa reducers de thunks en Redux, con otras palabras.

// Vuex: dos rutas separadas, exactamente como reducer y thunk en Redux
export default createStore({
  state: () => ({ lista: [], cargando: false }),
  mutations: {
    fijarLista(estado, lista) { estado.lista = lista },
    fijarCargando(estado, valor) { estado.cargando = valor },
  },
  actions: {
    async cargar({ commit }) {
      commit('fijarCargando', true)
      commit('fijarLista', await traerTareas())
      commit('fijarCargando', false)
    },
  },
})

Una diferencia técnica de fondo separa a Vuex de Redux desde el primer día, y de ella se derivará todo lo demás: el estado de Vue no es inmutable. Redux exige devolver objetos nuevos porque React necesita comparar referencias para saber qué cambió; Vue envuelve el estado en proxies y detecta la escritura en el momento en que ocurre, propiedad a propiedad. Ese detalle, que parece de implementación, es el que años después permitirá borrar media librería.

Lo que la reactividad garantiza gratis

Antes de mirar Pinia conviene ser preciso sobre qué problemas resolvía la ceremonia de Redux, porque no todos eran el mismo problema. Uno era arquitectónico: saber quién cambió qué y en qué orden. Otro era mecánico: enterarse de que algo cambió y averiguar qué componentes deben repintarse. El segundo es exactamente el que el sistema de reactividad de Vue resuelve por diseño, con seguimiento de dependencias de grano fino y sin que nadie escriba un selector.

ℹ️
Inmutabilidad no es un principio de Flux: es una técnica

Este es el matiz que más cuesta aceptar viniendo de Redux. Los tres principios del Nivel 2 hablan de una única fuente de verdad, de estado de solo lectura desde fuera y de cambios mediante funciones puras; ninguno exige literalmente que el estado sea inmutable. La inmutabilidad es la técnica con la que Redux implementa esos principios en un entorno que no sabe observar mutaciones, porque comparar referencias es la única forma barata que React tiene de detectar cambios. Vue observa la escritura directamente, así que puede honrar los mismos principios con un estado mutable y proxies. Confundir la técnica con el principio lleva a conclusiones falsas en ambas direcciones: a creer que Pinia abandonó Flux, o a creer que Redux era ceremonia gratuita. Ni una cosa ni la otra: son dos implementaciones de la misma idea sobre sustratos distintos.

flowchart TD
P[problema comun de estado compartido] --> RE[react no observa mutaciones]
P --> VU[vue observa con proxies]
RE --> IM[inmutabilidad y comparacion de referencias]
RE --> CE[reducers puros y acciones nombradas]
VU --> RX[seguimiento de dependencias fino]
VU --> PI[acciones normales que escriben directo]
IM --> F[mismo flujo unidireccional]
RX --> F
style P fill:#f9e2af,color:#11111b
style RE fill:#89b4fa,color:#11111b
style VU fill:#a6e3a1,color:#11111b
style F fill:#f38ba8,color:#11111b

Pinia: borrar la mitad sin perder el orden

Pinia observó que si las actions acaban siempre haciendo commit de una mutation, y si el framework ya sabe cuándo se escribe el estado, entonces la mutación es una capa de indirección que no compra nada. Así que la eliminó: en Pinia una acción puede ser asíncrona y escribir el estado directamente, y el store se declara con la misma sintaxis con la que se declara un componente.

import { defineStore } from 'pinia'

export const useTareas = defineStore('tareas', {
  state: () => ({ lista: [] as Tarea[], cargando: false }),

  // getters: estado derivado y memoizado, el equivalente de createSelector
  getters: {
    pendientes: (s) => s.lista.filter((t) => !t.hecha).length,
  },

  // acciones: asincronas y con escritura directa, sin mutation intermedia
  actions: {
    async cargar() {
      this.cargando = true
      this.lista = await traerTareas()
      this.cargando = false
    },
    completar(id: string) {
      const tarea = this.lista.find((t) => t.id === id)
      if (tarea) tarea.hecha = true
    },
  },
})

Lo llamativo es lo que sobrevive a la poda. Sigue habiendo una frontera de escritura clara —solo las acciones del store escriben, los componentes las invocan—, sigue habiendo estado derivado memoizado, sigue habiendo DevTools con historial de acciones y viaje en el tiempo, y sigue habiendo una única fuente de verdad por dominio. Lo que desapareció es la exigencia de declarar cada cambio como un objeto con tipo antes de aplicarlo, porque el proxy ya registra qué acción estaba en curso cuando la escritura ocurrió: el rastro se obtiene por observación en vez de por declaración.

💡
Muchos stores pequeños en vez de un árbol con módulos

La otra ruptura de Pinia es organizativa y pasa más desapercibida. Redux y Vuex parten de un árbol único que se subdivide en slices o módulos anidados; Pinia parte de stores independientes que se importan donde hacen falta y que pueden usarse entre sí. Es la misma decisión que tomó Zustand en el Nivel 6, y responde al mismo diagnóstico: la unicidad del árbol era útil para serializar el estado entero y para el viaje en el tiempo, pero imponía un espacio de nombres global y una configuración central que la mayoría de las apps no necesitaba. Con módulos ES, importar un store es tan trazable como leer un import, y el árbol único deja de ser la única forma de saber qué existe.

Qué se conserva cuando se quita la ceremonia

Hay además una segunda razón, menos citada y muy potente, para el cambio: el tipado. En Vuex, commit recibía el nombre de la mutación como texto y su carga como valor libre, y tipar eso con TypeScript exigía acrobacias de tipos que la mayoría de los equipos abandonaba. En Pinia, una acción es un método normal con parámetros normales, así que el editor la autocompleta y el compilador la verifica sin ayuda extra. Buena parte de la adopción de la nueva generación de stores —aquí y en el ecosistema de React— se explica por ese detalle práctico tanto como por la elegancia del modelo.

Conviene cerrar con honestidad sobre el precio, porque no es cero. Al permitir escritura directa desde cualquier acción, Pinia renuncia a una garantía que Redux sí ofrece: que todo cambio de estado sea un valor serializable, inspeccionable y reproducible fuera de contexto. Con acciones nombradas y reducers puros puedes reconstruir el estado exacto de una sesión ajena reproduciendo una lista de acciones; con escritura directa dependes de que el DevTools capture la instrumentación. Para una app mediana, la diferencia es teórica. Para un sistema donde el registro auditable de cambios es un requisito —finanzas, edición colaborativa, herramientas con deshacer profundo—, la diferencia es la razón por la que ese suelo del que hablaba el Nivel 6 sigue existiendo.

Se conserva

Fuente única por dominio, frontera de escritura, estado derivado memoizado, efectos aislados y herramientas de inspección.

⚠️

Se negocia

El cambio como valor serializable y reproducible fuera de contexto. Se recupera solo si el dominio lo exige, y con esfuerzo propio.

La lectura correcta de esa tabla no es que Pinia sea una versión descafeinada de Redux, sino que el diseño de un gestor de estado consiste en elegir qué garantías se compran y a qué precio, y que esa elección debería depender del dominio y no de la costumbre. Vue tomó la decisión de optimizar para el caso mayoritario y dejar la trazabilidad estricta como un extra construible; Redux tomó la contraria. Ninguna de las dos es un error: son dos puntos distintos de la misma curva, y saber en qué punto está tu aplicación es exactamente el juicio que este track te pide desarrollar.

La ceremonia que creías esencial era compensación por un entorno

El giro de Vuex a Pinia es el experimento natural que este track necesitaba para separar dos cosas que en React aparecían siempre juntas: los principios del patrón y las técnicas con las que un entorno concreto los implementa. Cuando Vue rehízo su gestor de estado desde cero, con una década de experiencia y sin la presión de imitar a nadie, no eliminó la fuente única de verdad, ni la frontera de escritura, ni la separación entre lo puro y lo impuro, ni el estado derivado memoizado: eliminó exactamente las piezas que existían para compensar que React no sabe observar mutaciones —los objetos de acción obligatorios, el reducer como intermediario, la copia inmutable en cada cambio— y las eliminó sin perder ninguna de las garantías que aquellas piezas protegían, porque su sistema de reactividad ya las proporcionaba por otra vía. Eso obliga a releer diecisiete niveles con una lente más fina: parte de lo que aprendiste como disciplina inviolable era, en realidad, el precio que un modelo de render concreto cobraba por darte trazabilidad, y ese precio cambia cuando cambia el sustrato. La conclusión no es que la ceremonia sobre, sino algo más exigente: que hay que saber en cada momento qué garantía compra cada línea de ceremonia, porque solo así puedes reconocer cuándo el entorno ya te la regala y estás pagando dos veces. Quien no hace esa distinción arrastra los rituales de un ecosistema a otro por inercia y escribe Pinia como si fuera Redux, o Redux como si fuera Pinia, y en ambos casos paga un coste que su entorno no le pedía. El patrón es el conjunto de garantías; la ceremonia es su factura en un entorno dado, y las facturas se revisan cada vez que te mudas.

⚔️ Descompón la ceremonia en garantías
  1. Toma un slice de Redux tuyo y escribe, para cada pieza —acción, reducer, selector, thunk—, qué garantía concreta compra. Sé específico: no vale decir orden.
  2. Reescribe ese slice como un store de Pinia con state, getters y actions, sin intentar imitar la estructura de Redux.
  3. Marca cuáles de las garantías de tu lista siguen presentes en la versión de Pinia y quién las proporciona ahora: el código o el framework.
  4. Identifica la única garantía que de verdad se pierde y decide si tu aplicación la necesitaba. Justifica la respuesta con un caso de uso, no con un principio.
  5. Escribe la misma funcionalidad con mutations al estilo Vuex y compara las tres versiones por número de conceptos, no por número de líneas.
  6. Resume en dos frases qué parte de tu disciplina con Redux es transferible a cualquier stack y qué parte era el peaje que React te cobraba.