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

Redux es agnóstico: el store es JavaScript puro

El store de Redux no contiene una sola línea de React: es un objeto de JavaScript con tres métodos que funciona igual en Node, en un worker o en una vista escrita en cualquier framework. Esta lección desmonta la confusión entre el patrón y su capa de enlace montando un contador completo sobre el DOM sin librería de interfaz alguna, muestra dónde empieza y dónde acaba el trabajo de react-redux, y explica por qué el problema difícil nunca fue el store sino traducir su suscripción al ciclo de render de una vista concreta. Es la puerta de entrada al nivel: si el núcleo es agnóstico, todo lo que sigue son dialectos del mismo patrón.

⏱ 17 min

Durante diecisiete niveles hemos hablado de Redux como si fuese una pieza de React, y esa costumbre esconde el hecho más importante de la librería: el store de Redux no contiene una sola línea de código de React. Es un objeto de JavaScript con tres métodos —getState, dispatch y subscribe— que podrías ejecutar en Node, en un worker, en una extensión de navegador o en una aplicación escrita en Vue sin cambiar absolutamente nada. Lo que llamamos la integración con React vive entero en un paquete aparte, react-redux, y su trabajo se reduce a traducir la suscripción del store al ciclo de render de una vista concreta. Este nivel consiste en soltar el andamio y mirar el patrón desnudo, y empieza por el reconocimiento más incómodo para quien aprendió los dos a la vez: React nunca fue necesario.

🎯 Al terminar esta lección sabrás
  • Ver el store de Redux como un objeto de JavaScript puro con tres métodos y ninguna dependencia de vista.
  • Montar un contador funcional sobre el DOM sin usar ninguna librería de interfaz.
  • Distinguir el núcleo del patrón de la capa de enlace que cada vista necesita construir.
  • Reconocer qué problemas resuelve react-redux y por qué ninguno de ellos pertenece a Redux.

El store no sabe que existe React

Abre el código fuente de Redux y cuenta las dependencias: no hay ninguna. El paquete entero son unos cientos de líneas que construyen un objeto con una función que devuelve el estado, otra que recibe acciones y otra que registra oyentes. No importa nada, no conoce el DOM, no sabe qué es un componente. Esta pobreza deliberada no es un accidente de implementación sino la decisión de diseño que explica el nivel entero: al no acoplarse a ninguna vista, Redux se volvió portable a todas.

📖

getState

Devuelve el estado actual. Una lectura síncrona de un objeto plano, sin hooks, sin contexto, sin suscripción implícita a nada.

📮

dispatch

Recibe una acción, la pasa por el reducer, guarda el resultado y avisa a los oyentes. Es la única escritura posible.

🔔

subscribe

Registra una función que se ejecutará tras cada despacho y devuelve otra para darse de baja. El observador clásico.

🧱

createStore

Combina reducer, estado inicial y middleware en el objeto anterior. Ninguna de sus piezas menciona una interfaz de usuario.

Esa superficie mínima tiene una consecuencia que conviene enunciar sin adornos: el patrón Flux es independiente de la tecnología de renderizado. El flujo unidireccional que estudiamos en el Nivel 1 —acción, reducer, estado nuevo, notificación— ocurre entero dentro del store, antes de que ninguna vista se entere. La vista es un suscriptor más, no un participante privilegiado del ciclo. Y si es un suscriptor más, puede ser cualquier cosa capaz de ejecutar una función cuando la avisan.

La lista de entornos donde ese objeto funciona sin cambios es más larga de lo que sugiere la costumbre. Un proceso de Node que orquesta un flujo de datos, un worker que mantiene el estado de una simulación fuera del hilo principal, el proceso principal de una aplicación de Electron, el servicio de fondo de una extensión de navegador, un juego que pinta sobre un lienzo, incluso un programa de terminal: en todos ellos el store se crea igual, se despacha igual y se suscribe igual, porque en ninguno de ellos hay nada que Redux necesite del entorno. Cuando alguien dice que Redux es pesado para su caso, casi siempre se refiere al coste del paquete de integración y de las convenciones alrededor, no a los dos kilobytes del núcleo.

📝
createStore está desaconsejado, pero el núcleo no ha cambiado

En el código actual verás configureStore de Redux Toolkit en lugar de createStore, que aparece marcado como desaconsejado desde hace varias versiones. Conviene no confundir ese cambio con una modificación del modelo: configureStore es un envoltorio que aplica valores por defecto sensatos —middleware de comprobaciones, integración con DevTools, soporte de Immer— y devuelve exactamente el mismo objeto con getState, dispatch y subscribe. Usamos aquí la forma cruda porque hace visible el núcleo sin adornos; en producción usa el envoltorio y recuerda que debajo sigue habiendo el mismo objeto agnóstico de siempre.

Un contador sin framework

La demostración más contundente cabe en treinta líneas. Aquí está el patrón completo —store, acciones, reducer, suscripción y render— gobernando dos botones y un texto sobre el DOM desnudo, sin React, sin build, sin componentes.

import { createStore } from 'redux'

type Estado = { valor: number }
type Accion = { type: 'incrementar' } | { type: 'decrementar' }

function contador(estado: Estado = { valor: 0 }, accion: Accion): Estado {
  switch (accion.type) {
    case 'incrementar': return { valor: estado.valor + 1 }
    case 'decrementar': return { valor: estado.valor - 1 }
    default: return estado
  }
}

const store = createStore(contador)

// la vista es una funcion que lee el estado y pinta: nada mas
const salida = document.getElementById('valor')!
function render() {
  salida.textContent = String(store.getState().valor)
}

// el enlace completo: suscribirse y pintar una vez al arrancar
store.subscribe(render)
render()

document.getElementById('mas')!.onclick = () => store.dispatch({ type: 'incrementar' })
document.getElementById('menos')!.onclick = () => store.dispatch({ type: 'decrementar' })

Antes de analizarlo, fíjate en la anatomía: las primeras quince líneas son el patrón —tipos, reducer, store— y son transferibles tal cual a cualquier entorno; las últimas ocho son la vista, y son lo único que habría que reescribir si mañana cambiaras de tecnología. Esa proporción no es casual y se mantiene en aplicaciones reales: la parte agnóstica es la mayor y la más valiosa, y la parte atada al framework es fina y sustituible.

Merece la pena detenerse en lo que no aparece en ese fragmento. No hay Provider, porque el store es una variable del módulo y cualquiera que lo importe lo tiene. No hay useSelector, porque leer es llamar a getState. No hay connect, porque conectar es llamar a subscribe. Las tres piezas que en React parecen fundamentales resultan ser andamiaje para un problema que en el DOM plano no existe: cómo hacer que un árbol de componentes con render declarativo reaccione a un objeto mutable de fuera.

flowchart TD
ST[store de redux en javascript puro] --> API[getState dispatch subscribe]
API --> EV[capa de enlace vanilla]
API --> ER[capa de enlace react-redux]
API --> EO[capa de enlace de otro framework]
EV --> D1[nodos del dom]
ER --> D2[componentes de react]
EO --> D3[componentes de vue o svelte]
style ST fill:#f38ba8,color:#11111b
style API fill:#f9e2af,color:#11111b
style EV fill:#a6e3a1,color:#11111b
style ER fill:#89b4fa,color:#11111b

La capa de enlace es lo único que cambia

Si el store es idéntico en todas partes, la pregunta interesante es qué hace exactamente el paquete que lo conecta a una vista. La respuesta de react-redux tiene tres partes, y ninguna trata sobre gestión de estado. La primera es la distribución: el Provider mete el store en el contexto para no obligar a importarlo en cada archivo, algo irrelevante cuando el módulo se importa directamente. La segunda es la selección: useSelector ejecuta tu función sobre el estado y decide si el resultado cambió, para no volver a renderizar cuando la porción que te interesa sigue igual. La tercera, y la más sutil, es la sincronización con el modo concurrente de React mediante useSyncExternalStore, que garantiza que un componente no lea un estado a medio actualizar mientras el renderizador interrumpe y reanuda trabajo.

ℹ️
El tearing es un problema de React, no de Redux

La sincronización que resuelve useSyncExternalStore merece nombre propio porque explica por qué la capa de enlace no es trivial. Cuando React renderiza de forma interrumpible, dos componentes del mismo árbol pueden leer la fuente externa en instantes distintos y pintar versiones distintas del mismo dato: eso es el desgarro o tearing. El store nunca es inconsistente —siempre tiene un único estado válido—, pero la vista puede mostrar una mezcla si lee sin coordinación. Por eso el enlace de React es el más elaborado de todos los que veremos en el nivel: no compensa una carencia de Redux, compensa una libertad de React. En una vista síncrona como el DOM plano, o en el sistema de reactividad de Vue, ese problema sencillamente no se plantea, y su capa de enlace es proporcionalmente más simple.

El mismo store, sin tocar una línea, funciona en un entorno donde ni siquiera hay pantalla. Este fragmento es un proceso de Node que reacciona a eventos y guarda un registro, y usa el patrón por sus garantías de trazabilidad, no por su relación con ninguna interfaz.

// mismo store, cero vista: la trazabilidad es util aunque no haya pantalla
const store = createStore(contador)

// el suscriptor ahora es un registro, no un nodo del dom
store.subscribe(() => {
  console.log(new Date().toISOString(), 'estado', store.getState())
})

process.on('SIGUSR2', () => store.dispatch({ type: 'incrementar' }))

Este reparto explica una asimetría que confunde a mucha gente: el paquete de integración de React es más grande y más complejo que el propio Redux. No es que Redux delegue trabajo en él; es que traducir una suscripción imperativa al modelo de render de React es un problema genuinamente difícil, mientras que el patrón que hay detrás del store es casi trivial. Cuando en las próximas lecciones veamos NgRx, Pinia o Nanostores, lo que cambiará entre ellos no será el flujo unidireccional —ese será siempre el mismo— sino precisamente esta capa: cómo cada framework se entera de que el estado cambió.

🧩

Núcleo del patrón

Acciones, reducer, store, suscripción. Idéntico en todos los entornos y transferible sin cambios entre plataformas.

🔌

Capa de enlace

Distribución, selección y sincronización con el renderizador. Cambia por completo en cada framework y es lo único que cambia.

Establecer esa frontera con nitidez tiene un efecto inmediato sobre cómo organizas el código. Si el núcleo es agnóstico, no hay razón para que tus reducers, tus acciones y tus selectores importen nada de React, y mantenerlos limpios de esa dependencia es una regla barata con un rendimiento alto: puedes probarlos en Node sin montar un renderizador, reutilizarlos si parte de la lógica se mueve a un worker o al servidor, y sobre todo sabes en todo momento qué parte de tu aplicación se juega en una migración de framework y cuál no. La mayoría de las bases de código mezclan ambas capas sin necesidad, y el precio no se paga hasta el día en que hay que mudarse.

Aprendiste dos cosas a la vez y creíste que eran una

La razón por la que esta lección resulta reveladora a casi todo el mundo es un accidente pedagógico con consecuencias arquitectónicas duraderas: la inmensa mayoría de quienes usamos Redux lo aprendimos el mismo mes que aprendimos React, en el mismo tutorial, con los mismos ejemplos, y por eso quedaron fundidos en una sola idea mental donde en realidad hay dos completamente separadas. La consecuencia de esa fusión no es teórica. Quien cree que Redux es una pieza de React piensa que abandonar React implica abandonar el patrón, y que adoptar otro framework significa aprender un modelo de estado distinto desde cero; y por eso llega a Angular o a Vue tratando NgRx o Pinia como novedades que hay que estudiar, cuando son el mismo flujo unidireccional con otro vocabulario y otra capa de enlace. Al revés también hace daño: quien confunde patrón con librería atribuye a Redux virtudes que son de la disciplina —la trazabilidad, la predecibilidad, el estado como valor derivado de una secuencia de eventos— y las da por perdidas en cuanto cambia de herramienta, cuando esas virtudes se conservan íntegras porque no vivían en el paquete sino en la forma de razonar. Separar las dos capas es lo que convierte diecisiete niveles de Redux en conocimiento transferible en lugar de en una habilidad atada a un ecosistema: el store es un objeto con tres métodos, la vista es un suscriptor, y todo lo demás es negociación entre ambos. Quien interioriza esa frontera deja de preguntar qué gestor de estado usa este framework y empieza a preguntar cómo enlaza este framework con una fuente externa de verdad, que es la única pregunta cuya respuesta cambia de verdad al cruzar de un stack a otro.

⚔️ Construye tu propia capa de enlace
  1. Crea un store con createStore en un archivo sin ninguna importación de React y comprueba que arranca en Node con un simple node.
  2. Despacha tres acciones desde la terminal e imprime getState tras cada una. Verifica que el flujo unidireccional funciona sin vista alguna.
  3. Escribe una función enlazar que reciba el store, un selector y un callback, y solo invoque el callback cuando el valor seleccionado cambie de identidad.
  4. Usa esa función para pintar dos porciones distintas del estado en dos nodos del DOM y confirma que cada nodo se repinta solo cuando su porción cambia.
  5. Añade un tercer nodo que dependa de un dato que nunca despachas y comprueba que no se repinta jamás. Acabas de reimplementar el núcleo de useSelector.
  6. Escribe en tres frases qué parte de tu código sería idéntica si mañana montaras esa misma app en Vue, y qué parte tendrías que reescribir entera.