Qué es el middleware: la cadena entre el dispatch y el reducer
El reducer de Redux es una función pura: no pide a la red, no consulta el reloj, no despacha nada. Todo eso —los efectos— tiene que ocurrir en otra parte, y esa parte es el middleware: una cadena de funciones que intercepta cada acción entre el dispatch y el reducer. Esta lección disecciona la firma curificada de tres capas —store, next, action—, explica por qué next cede el paso al siguiente eslabón mientras dispatch reinyecta desde el principio, y muestra cómo RTK compone la cadena con getDefaultMiddleware, que en 2026 ya trae thunk y las guardas de inmutabilidad y serializabilidad de serie.
Un reducer es una promesa de pureza: recibe el estado y una acción, devuelve el estado siguiente y no hace nada más —ni pide a la red, ni consulta el reloj, ni escribe en disco—. Esa castidad es lo que vuelve a Redux predecible, depurable y viajable en el tiempo, pero deja abierta una pregunta incómoda: si el reducer no puede tener efectos, ¿dónde ocurren? La respuesta es el middleware, una cadena de funciones interpuesta entre el dispatch y el reducer por la que desfila, una a una, cada acción del sistema. Ahí —y solo ahí— viven el registro, la asincronía, la telemetría y todo lo que ensucia. Comprender el middleware es comprender que Redux no prohíbe los efectos: los confina a un lugar observable, ordenado y componible.
- Entender por qué el reducer puro no puede alojar efectos y por qué el middleware sí.
- Diseccionar la firma curificada de tres capas
store => next => action. - Distinguir
next(action), que cede al siguiente eslabón, destore.dispatch, que reinicia la cadena. - Componer la cadena con
getDefaultMiddlewarey saber qué trae RTK de serie en 2026.
El reducer no puede; el middleware sí
El contrato del reducer es de una estrechez deliberada: una función (state, action) => state que ha de ser determinista, síncrona y libre de efectos. Esa restricción no es un capricho, es el pago por adelantado de todos los superpoderes de Redux: el time-travel, la repetición de acciones, el hot reload y los tests que corren sin montar un mundo. Un solo efecto dentro del reducer envenenaría todo ese aparato: repetir una tanda de acciones volvería a disparar peticiones de red, un test golpearía servicios reales y el viaje en el tiempo dejaría de ser fiel, porque rebobinar el estado no rebobina un correo ya enviado.
Pero ninguna aplicación real vive sin efectos, y hay dos hogares equivocados para ellos. Repartirlos por los componentes disuelve el flujo único y observable que las arquitecturas unidireccionales de este bloque prometían. Contrabandearlos dentro del reducer rompe la pureza y con ella las garantías. Redux abre un tercer lugar: una capa de intercepción situada entre la intención —el dispatch— y la mutación —el reducer—. El efecto no se prohíbe ni se esconde; se traslada a un escenario con nombre por el que pasa cada acción y donde cada cosa sucia deja rastro.
La forma y hasta la palabra vienen de los frameworks de servidor: Express, Koa, Rack, Ring o ASP.NET, donde cada petición HTTP atraviesa una tubería de funciones que pueden registrar, autenticar, comprimir o cortar el paso. Redux cosifica la acción como esa “petición” que viaja por la tubería, y por eso el modelo mental se transfiere sin fricción: quien ha escrito middleware de servidor reconoce el patrón al instante. La acción entra por un extremo, cada función tiene la oportunidad de mirarla o alterarla, y al final —si nadie la detuvo— llega al reducer, que es el único que decide el nuevo estado.
Conviene fijar la frontera con precisión, porque es sutil y todo el nivel depende de ella. El middleware puede tener cuantos efectos quiera —ese es su oficio—, pero el reducer que espera al final de la cadena sigue siendo sagradamente puro. El efecto ocurre en el tránsito; la decisión sobre el estado nuevo, en el destino. Un thunk pide a la red y, cuando la respuesta llega, despacha una acción plana que el reducer consume sin enterarse de que hubo asincronía de por medio. Esa división del trabajo —la cadena ensucia, el reducer decide en limpio— es lo que permite que las garantías del reducer sobrevivan intactas en un mundo saturado de efectos, y es también la razón de que las tres librerías de este nivel se diferencien solo en cómo pueblan el tránsito, nunca en tocar el destino.
La firma de tres capas
Un middleware es una función de tres flechas anidadas, y cada flecha se invoca en un momento distinto del ciclo de vida. La capa externa, store, se llama una sola vez al montar el store y recibe una API mínima con getState y dispatch. La capa media, next, se llama una vez al ensamblar la cadena y es la puerta de entrada del eslabón siguiente. La capa interna, action, se ejecuta cada vez que una acción atraviesa este punto. Esa curificación es lo que permite que la cadena se arme una vez y luego procese miles de acciones sin volver a construirse.
import type { Middleware } from "@reduxjs/toolkit";
// Tres capas curificadas: store (una vez), next (al ensamblar), action (cada vez).
export const logger: Middleware = (store) => (next) => (action) => {
const tipo = (action as { type: string }).type;
console.group(tipo);
console.log("estado antes:", store.getState());
const resultado = next(action); // cede la accion al siguiente eslabon
console.log("estado despues:", store.getState());
console.groupEnd();
return resultado; // preserva el valor de retorno de la cadena
};
Cada eslabón dispone de exactamente tres verbos. Puede observar el presente con store.getState() antes y después de dejar pasar la acción. Puede ceder el paso con next(action), que empuja la acción hacia el eslabón siguiente y, en última instancia, hacia el reducer. Y puede iniciar un viaje nuevo con store.dispatch(otra), que introduce una acción distinta por la boca de la cadena. Devolver el resultado de next(action) conserva el valor de retorno que atraviesa la tubería, un detalle que parece cosmético hasta que un thunk lo aprovecha para devolver una promesa, como verás en la lección siguiente.
Hay una simetría fácil de pasar por alto: cada acción recorre la cadena dos veces. A la ida, de fuera hacia dentro, cada eslabón ejecuta lo que puso antes de su next; al alcanzar el reducer y regresar, de dentro hacia fuera, ejecuta lo que puso después. Es el modelo cebolla de las tuberías de servidor, y el logger de arriba lo encarna: su getState de antes y su getState de después son esos dos momentos del mismo viaje. Comprender que el return de cada eslabón sube de vuelta hacia arriba explica por qué una promesa devuelta por un thunk llega intacta hasta el componente que despachó: viaja de regreso por la misma tubería que la acción bajó.
Ese poder de intervenir en el tránsito no se agota en observar. Un middleware puede traducir: interceptar una acción abstracta y reemplazarla por otras más concretas antes de dejarla seguir, de modo que el resto de la cadena y el reducer solo vean el vocabulario que entienden.
import type { Middleware } from "@reduxjs/toolkit";
// Traduce una accion de alto nivel en dos acciones planas que el reducer ya conoce.
export const expandirAtajo: Middleware = () => (next) => (action) => {
const a = action as { type: string };
if (a.type === "carrito/vaciarYCerrar") {
next({ type: "carrito/vaciar" }); // primero una
return next({ type: "ui/cerrarPanel" }); // luego otra, devolviendo su resultado
}
return next(a); // cualquier otra accion, sin tocar
};
next(action) entrega la misma acción al siguiente eslabón; el viaje termina en el reducer. store.dispatch(action) la devuelve al principio de la cadena, reejecutando todos los middleware desde arriba. Llamar a dispatch con la misma acción sin condición es un bucle infinito instantáneo; olvidar llamar a next hace que la acción se esfume sin llegar jamás al reducer. La regla mnemónica: next avanza, dispatch reinicia. Usa next para el flujo normal y reserva dispatch para inyectar acciones nuevas nacidas de un efecto.
flowchart LR D[dispatch de una accion] --> A[logger] A -->|next| B[thunk] B -->|next| C[guarda serializable] C -->|next| R[reducer puro] R --> E[nuevo estado en el store] A -.dispatch nuevo.-> D style R fill:#a6e3a1,color:#11111b style D fill:#89b4fa,color:#11111b
Componer la cadena
El Redux clásico ensamblaba la tubería a mano con applyMiddleware(a, b, c), pero en 2026 nadie escribe eso: configureStore de Redux Toolkit lo hace por ti. La opción middleware recibe una función a la que se le pasa getDefaultMiddleware, y lo idiomático es partir de esa base y añadir los tuyos con concat —para que corran después— o prepend —para que corran antes—. Nunca mutes la tupla que devuelve; es de solo lectura por diseño, para que el orden sea explícito y no un efecto colateral de un push.
import { configureStore } from "@reduxjs/toolkit";
import { logger } from "./logger";
import { rootReducer } from "./reducers";
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(logger), // logger corre despues de los de serie
});
Lo que getDefaultMiddleware trae de serie no es decorativo. Incluye redux-thunk —el protagonista de la próxima lección— y dos guardas que solo actúan en desarrollo: la de inmutabilidad, que grita si mutas el estado fuera de un reducer, y la de serializabilidad, que grita si una promesa, una fecha o una función se cuela en una acción o en el estado. Esas dos guardas convierten en alarmas de ejecución los principios que este track defendió antes en abstracto: el estado es inmutable y las acciones son datos planos y serializables. El orden importa: el eslabón más a la izquierda es el más externo y el primero en ver la acción a la ida, y el último en verla a la vuelta.
Y ese orden no es cosmético. Si colocas el logger con prepend, antes que thunk, verá pasar la función del thunk —una acción que ni siquiera es serializable y que aún no se ha resuelto en acciones planas—; si lo pones con concat, después, verá solo las acciones planas que el thunk despacha, ya legibles. Ninguno de los dos órdenes es incorrecto en abstracto: dependen de qué quieras observar. Esa sensibilidad al orden es justo la razón de que RTK te obligue a componer con concat y prepend sobre una tupla inmutable, en lugar de dejarte empujar a un array mutable: el orden es una decisión de diseño y merece quedar explícita en el sitio donde se toma.
getDefaultMiddleware acepta opciones para apagar la comprobación de serializabilidad o la de inmutabilidad, y a veces hace falta —una fecha o una instancia de clase legítima en el estado—. Pero apagarlas no es gratis: renuncias a la persistencia trivial del store, al time-travel fiel y a la garantía de que una acción se puede registrar y repetir. Antes de silenciar la guarda, pregúntate si el valor no serializable debería vivir fuera de Redux —en una ref, en un módulo— y entrar al store solo como un identificador. Silenciar la alarma es a veces correcto; hacerlo sin mirar qué señalaba casi nunca.
Observar
Leer store.getState() antes y después de next para registrar, medir o auditar sin alterar nada del flujo.
Desviar
Transformar o reemplazar la acción antes de pasarla, o traducir una acción abstracta en otra más concreta.
Diferir
Retener la acción, esperar una promesa o un temporizador, y despachar el resultado más tarde con dispatch.
Tragar
No llamar a next y anular la acción, útil para throttling, feature flags o bloquear efectos en ciertos entornos.
La verdadera invención de Redux no fue el reducer —que no es más que un fold sobre un flujo de acciones— ni el store —que no es más que un observable de la lección 3—, sino la decisión de convertir el efecto secundario en una etapa de primera clase, con nombre y dirección, en lugar de un peligro ambiental esparcido por toda la interfaz. Al forzar que cada efecto atraviese una única tubería curificada, indexada por el mismo flujo de acciones que alimenta al reducer, Redux transforma el difuso “en algún sitio, de algún modo, ocurre algo asíncrono” en el preciso “en este eslabón, ante esta acción, se dispara este efecto, y aquí queda su traza”. Esa mudanza del efecto —de lo implícito y disperso a lo explícito y localizado— es la misma disciplina que el nivel 1 pedía para el estado, ahora aplicada al cambio en lugar de al dato. Todo lo que viene después en este nivel —thunk, saga, listener— es una ergonomía distinta sobre esta misma costura: discrepan en cómo escribes el efecto, jamás en dónde vive. Por eso el salto conceptual que de verdad importa no es aprender ninguna de las tres librerías, sino dejar de leer dispatch como “cambia el estado” y empezar a leerlo como “emite un hecho que primero recorre una cadena de interceptores y solo entonces, quizá, alcanza al reducer”. Quien interioriza eso ya no ve middleware; ve la forma canónica en que un sistema honesto admite que tiene efectos sin dejar que lo gobiernen.
- Escribe el
loggerde la lección y despacha tres acciones; observa en consola el estado antes y después de cada una y confirma que el reducer corre entre ambosconsole.log. - Escribe un middleware que trague una acción concreta —que nunca llame a
nextsi eltypecoincide— y verifica que el reducer jamás la ve. - Provoca un bucle infinito a propósito llamando a
store.dispatch(action)sin condición dentro de un middleware; luego arréglalo cambiándolo pornext(action)y explica la diferencia. - Inspecciona el contenido de
getDefaultMiddleware()en desarrollo y dispara la guarda de serializabilidad despachando una acción que lleve unDateen su payload. - Coloca el
loggerconprependen vez deconcaty compara la traza: ¿ve la acción antes o después de quethunkla procese? - Escribe el middleware
expandirAtajoque traduce una acción en dos y confirma que el reducer solo recibe las dos acciones planas, nunca la abstracta. - Añade
console.logantes y después denexten un middleware y verifica el modelo cebolla: el orden de impresión es ida de fuera a dentro, vuelta de dentro a fuera.