La lección anterior terminó con una lista precisa de lo que el contexto no resuelve: herramientas de depuración, un punto único donde interceptar cambios, viaje en el tiempo, selección granular y una convención compartida para la lógica asíncrona. Redux existe exactamente por esa lista. Y conviene decirlo desde la primera línea para deshacer el malentendido más común: Redux no es una forma más rápida de compartir estado, es una forma más disciplinada de cambiarlo. Lo que compras con él no es velocidad, es predecibilidad y capacidad de diagnóstico. En esta lección vas a entender qué problema resuelve Redux, sus tres principios y por qué hacen la aplicación depurable, su relación directa con el useReducer que ya escribiste en 05-05, por qué Redux Toolkit es hoy la forma oficial de usar Redux y qué cambia respecto del Redux clásico que todavía verás en proyectos antiguos. Después montarás el almacén de CicloUrbano con configureStore, lo proveerás en main.jsx sin romper el orden de proveedores ni el enrutador, e instalarás Redux DevTools para ver el historial de acciones. Al terminar, el almacén existirá y funcionará, pero apenas se consumirá: modelarlo es cosa de 07-04 y conectarlo a los componentes, de 07-05.
Contenido
- Qué es Redux y qué problema resuelve
- Los tres principios
- Por qué esos principios hacen la aplicación predecible y depurable
- Relación con
useReducer: mismo modelo, distinta escala - El flujo unidireccional
- Redux Toolkit: la forma oficial de usar Redux hoy
- Cómo era el Redux clásico (código heredado que hay que saber leer)
- Instalación
- El almacén de CicloUrbano:
src/almacen/almacen.js - El middleware por defecto y sus comprobaciones de desarrollo
- Proveer el almacén en
main.jsx - Redux DevTools: el argumento más fuerte
- Cuándo NO usar Redux
- Qué es Redux y qué problema resuelve
Redux es un contenedor de estado predecible: un objeto JavaScript que guarda todo el estado del cliente de la aplicación y que solo permite cambiarlo enviando acciones, objetos que describen qué ha ocurrido. Ninguna otra vía de modificación existe.
El problema que resuelve no es «compartir datos entre componentes lejanos» —eso lo hace el contexto, y más barato—. El problema es este otro, que aparece cuando una aplicación crece:
Cuando algo va mal en la pantalla, no sabes qué cambió el estado, cuándo lo cambió ni quién lo provocó.
En una aplicación con quince componentes que llaman a setAlgo desde manejadores, efectos y callbacks de peticiones, reconstruir la secuencia de sucesos que llevó al fallo es arqueología. Redux ataca eso convirtiendo cada cambio en un registro explícito y ordenado: una lista de acciones, con su carga útil y su marca temporal, que puedes recorrer hacia atrás.
La segunda cosa que resuelve, más prosaica pero igual de real: da una convención. En un equipo de seis personas, «dónde va este dato y cómo se cambia» deja de ser una discusión por cada funcionalidad.
- Los tres principios
Redux se apoya en tres reglas. No son recomendaciones: son las que hacen posible todo lo demás.
Principio 1: fuente única de verdad
Todo el estado de la aplicación vive en un único objeto, dentro de un único almacén.
// Forma aproximada del estado de CicloUrbano (se define en 07-04)
{
reservas: { entidades: {…}, ids: […], estadoCarga: 'inactivo', error: null },
catalogo: { tipo: 'todos', termino: '', orden: 'modelo' },
sesion: { usuario: null, cargando: true }
}Consecuencias prácticas: el estado completo se puede serializar y adjuntar a un informe de fallo; se puede persistir en localStorage y restaurar; se pueden escribir pruebas partiendo de un estado inicial concreto; y no hay dos sitios donde el mismo dato pueda decir cosas distintas.
Principio 2: el estado es de solo lectura
La única forma de cambiar el estado es despachar una acción, un objeto plano que describe qué ha ocurrido.
Nadie escribe estado.reservas[0].estado = 'confirmada'. La consecuencia es que existe un cuello de botella deliberado: todos los cambios pasan por el mismo sitio, así que ese sitio puede registrarlos, medirlos, interceptarlos o repetirlos.
Principio 3: los cambios se hacen con funciones puras
Un reductor es una función pura (estado, accion) => nuevoEstado. No modifica sus argumentos, no hace peticiones, no lee la hora ni genera identificadores aleatorios, y con las mismas entradas devuelve siempre la misma salida.
Es literalmente la definición que aprendiste en 05-05 para reductorReservas.
- Por qué esos principios hacen la aplicación predecible y depurable
Los tres principios juntos producen una propiedad muy concreta:
Estado inicial + lista de acciones = estado actual. Siempre, sin excepciones.
De ahí sale todo lo demás:
| Propiedad | Cómo la habilitan los principios |
|---|---|
| Reproducir un fallo | Si guardas el estado inicial y las acciones, puedes reproducir exactamente la sesión del usuario que falló |
| Viaje en el tiempo | Como los reductores son puros, volver a aplicar las primeras N acciones da el estado en el momento N |
| Registro automático | El cuello de botella del principio 2 permite registrar cada cambio sin tocar los componentes |
| Pruebas triviales | Probar un reductor es llamar a una función con dos argumentos y comparar la salida (Módulo 9) |
| Deshacer / rehacer | Con estados inmutables, guardar los anteriores es guardar referencias |
| Depuración por diferencias | DevTools muestra el «antes y después» de cada acción, así que el cambio inesperado se localiza en segundos |
Y el precio, que hay que decir igual de claro: más ceremonia. Cambiar un dato deja de ser una línea y pasa a ser una acción, un caso en un reductor y un selector. Esa ceremonia es la que se paga a cambio de la tabla de arriba. Si tu aplicación no necesita nada de esa tabla, no la pagues.
- Relación con
useReducer: mismo modelo, distinta escala
useReducer: mismo modelo, distinta escalaEsto ya se anunció en 05-05: el modelo de Redux es exactamente el de useReducer. Compara.
// Lo que escribiste en 05-05
const [estado, despachar] = useReducer(reductorReservas, ESTADO_INICIAL_RESERVAS);
despachar({ tipo: 'reserva_confirmada', idReserva: 'res-01' });
// Lo mismo en Redux
const estado = almacen.getState();
almacen.dispatch({ type: 'reservas/reservaConfirmada', payload: 'res-01' });Un reductor de Redux y tu reductorReservas son la misma clase de función. Las diferencias son de alcance, no de concepto:
useReducer |
Redux | |
|---|---|---|
| Dónde vive el estado | Dentro del árbol de React, en un componente | En un objeto independiente, fuera de React |
| Alcance | Un subárbol | Toda la aplicación |
| Acceso | useContext desde los descendientes del proveedor |
useSelector desde cualquier componente |
| Nombre del campo de tipo | Libre; en el curso usamos tipo |
Obligatoriamente type |
| Carga útil | Campos libres en la acción | Convención payload |
| Interceptar cambios | No | Middleware |
| Herramientas | Ninguna | DevTools |
| Combinar dominios | Un reductor por proveedor | Varios reductores en un almacén |
Presta atención a dos filas: en Redux el campo se llama type (en inglés y sin negociación, porque lo exige la biblioteca y sus herramientas), y la carga útil va por convención en payload. En el resto del curso hemos usado tipo en español porque era código nuestro; a partir de aquí, cuando el objeto sea una acción de Redux, se usan type y payload. Es la misma regla de siempre: las APIs y palabras clave no se traducen.
Que el estado viva fuera de React tiene consecuencias que ya se apuntaron en 07-02: el almacén puede leerse desde una utilidad, desde un interceptor de peticiones o desde una prueba sin montar ningún componente, y sobrevive al desmontaje de cualquier parte del árbol.
- El flujo unidireccional
flowchart LR
A["Vista<br/>PaginaReservas"] -- "1. dispatch(accion)" --> B["Almacén"]
B -- "2. (estado, accion)" --> C["Reductor<br/>función pura"]
C -- "3. nuevo estado" --> B
B -- "4. notifica" --> D["Suscriptores<br/>useSelector"]
D -- "5. repinta si su porción cambió" --> A
E["Middleware<br/>registro · asincronía · DevTools"] -.- B
Los cinco pasos, con nombres concretos de CicloUrbano:
- El usuario pulsa «Confirmar» en
PanelReservasy el manejador despacha{ type: 'reservas/reservaConfirmada', payload: 'res-01' }. - El almacén pasa la acción por el middleware —que puede registrarla, retrasarla o transformarla— y luego llama al reductor raíz con el estado actual y la acción.
- El reductor devuelve un estado nuevo; nunca modifica el que recibió.
- El almacén guarda el estado nuevo y notifica a sus suscriptores.
- Cada componente suscrito comprueba si su porción cambió y se repinta solo si es así.
«Unidireccional» significa que las flechas nunca van al revés: una vista no modifica el estado directamente, ni un reductor provoca una navegación, ni el almacén llama a un componente. Esa disciplina es lo que hace que el paso 5 sea el único sitio donde mirar cuando la pantalla no muestra lo esperado.
Fíjate en el paso 5 y en la palabra «su porción»: ahí está la selección granular que el contexto no podía dar. El mecanismo exacto es useSelector, y es el tema de 07-05.
- Redux Toolkit: la forma oficial de usar Redux hoy
Redux tiene más de diez años y arrastra una reputación de verbosidad muy merecida... referida a cómo se escribía en 2016. Desde 2019, Redux Toolkit (RTK) es la forma oficial y recomendada de usar Redux, y así lo dice la propia documentación del proyecto. Este curso enseña Redux exclusivamente con RTK.
Qué incluye el paquete:
| Utilidad | Para qué sirve | Dónde se ve |
|---|---|---|
configureStore |
Crea el almacén con buenos valores por defecto y DevTools ya conectadas | Esta lección |
createSlice |
Genera reductor y creadores de acción a partir de una descripción | 07-04 |
createAsyncThunk |
Lógica asíncrona con los tres estados (pending/fulfilled/rejected) |
07-04 |
createSelector |
Selectores derivados memorizados (viene de Reselect) | 07-04 y 07-05 |
createEntityAdapter |
Estado normalizado por id, con operaciones ya escritas | 07-04 |
| Immer, incluido | Permite escribir código «que muta» sin mutar de verdad | 07-04 |
| RTK Query, incluido | Caché de datos de servidor, alternativa a TanStack Query | 07-06 |
Lo que cambia respecto del Redux clásico:
| Redux clásico (2016) | Con Redux Toolkit | |
|---|---|---|
| Boilerplate | Constantes de tipo, creadores de acción, switch y combineReducers, cada cosa en su fichero |
Un createSlice genera acciones y reductor a la vez |
| Inmutabilidad | A mano, con ... anidados; un olvido es un fallo silencioso |
Immer: escribes estado.reservas.push(x) y sigue siendo inmutable |
| Asincronía | redux-thunk instalado y configurado aparte, o redux-saga |
createAsyncThunk, incluido y con patrón definido |
| Configuración de DevTools | window.__REDUX_DEVTOOLS_EXTENSION__ a mano en createStore |
Conectadas por defecto en desarrollo |
| Comprobaciones | Ninguna: mutar el estado por error no avisa | Avisos en desarrollo si mutas o guardas algo no serializable |
| Tamaño | redux es diminuto, pero se acaban sumando 3-4 paquetes |
RTK pesa más, y a cambio sustituye a todos ellos |
| Ficheros por funcionalidad | 3 o 4 (tipos.js, acciones.js, reductor.js, selectores.js) |
1 (sliceX.js) |
La única fila que juega en contra de RTK es el tamaño: RTK más react-redux rondan los 15-20 kB comprimidos frente a los ~2 kB de redux a secas. A cambio te ahorras Reselect, redux-thunk, la configuración de DevTools y una cantidad considerable de código propio. En cualquier aplicación real, el cambio compensa.
- Cómo era el Redux clásico (código heredado que hay que saber leer)
⚠️ Este apartado es exclusivamente para saber leer proyectos antiguos. El código que verás aquí no debe escribirse nunca en código nuevo. Se incluye porque te lo vas a encontrar en aplicaciones que llevan años en producción y porque explica de dónde vienen los conceptos que RTK automatiza. Después de este apartado no volverá a aparecer en el curso.
// ⚠️ CÓDIGO HEREDADO — NO ESCRIBIR ASÍ HOY
// tipos.js
export const RESERVA_CONFIRMADA = 'RESERVA_CONFIRMADA';
// acciones.js
export function confirmarReserva(idReserva) {
return { type: RESERVA_CONFIRMADA, payload: idReserva };
}
// reductor.js
import { RESERVA_CONFIRMADA } from './tipos.js';
const estadoInicial = { reservas: [] };
function reductorReservas(estado = estadoInicial, accion) {
switch (accion.type) {
case RESERVA_CONFIRMADA:
return {
...estado,
reservas: estado.reservas.map((reserva) =>
reserva.id === accion.payload ? { ...reserva, estado: 'confirmada' } : reserva
)
};
default:
return estado;
}
}
// almacen.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const reductorRaiz = combineReducers({ reservas: reductorReservas, catalogo: reductorCatalogo });
const almacen = createStore(
reductorRaiz,
applyMiddleware(thunk)
);Qué mirar en este fragmento cuando te lo encuentres:
- Las constantes de tipo existían para evitar erratas en cadenas de texto repartidas por varios ficheros. RTK las elimina:
createSlicegenera el tipo a partir del nombre. - El
switchcondefault: return estadoes obligatorio en Redux clásico, porque cada acción pasa por todos los reductores. Ojo con la diferencia respecto delreductorReservasde 05-05, donde eldefaultlanzaba un error: allí era correcto porque solo llegaban las acciones de ese reductor. - Los
...anidados son la inmutabilidad a mano. Con dos niveles ya son difíciles de leer y con tres son un foco de fallos. combineReducersexplícito yapplyMiddleware(thunk): los dos desaparecen conconfigureStore, que hace lo mismo por dentro.- Es habitual encontrarlo junto a
connect,mapStateToPropsymapDispatchToProps, la API previa a los hooks. Se explica en 07-05, y también solo para leer.
Todo lo que hace ese código lo hace RTK con una fracción del texto, sin posibilidad de olvidar un default ni de mutar por accidente.
- Instalación
Dos paquetes y para qué sirve cada uno:
| Paquete | Qué aporta |
|---|---|
@reduxjs/toolkit |
Redux en sí, más configureStore, createSlice, createAsyncThunk, createSelector, createEntityAdapter, Immer y RTK Query |
react-redux |
El pegamento con React: <Provider>, useSelector, useDispatch |
No instales redux, redux-thunk ni reselect por separado. Vienen incluidos en RTK, y añadirlos aparte puede acabar en dos copias de Redux en el mismo paquete final.
- El almacén de CicloUrbano:
src/almacen/almacen.js
src/almacen/almacen.jsconfigureStore es el punto de entrada. Recibe un objeto de configuración cuya clave obligatoria es reducer.
// src/almacen/almacen.js
import { configureStore } from '@reduxjs/toolkit';
import reductorReservas from '../funcionalidades/reservas/sliceReservas.js';
import reductorCatalogo from '../funcionalidades/catalogo/sliceCatalogo.js';
import reductorSesion from '../funcionalidades/sesion/sliceSesion.js';
export const almacen = configureStore({
// El mapa de reductores define la FORMA del estado global:
// estado.reservas · estado.catalogo · estado.sesion
reducer: {
reservas: reductorReservas,
catalogo: reductorCatalogo,
sesion: reductorSesion
}
});Tres cosas que hace configureStore sin que tengas que pedirlas:
- Combina los reductores. El objeto que pasas en
reducerse convierte internamente en uncombineReducers, así queestado.reservases lo que devuelvareductorReservas. - Configura el middleware por defecto, incluido el necesario para thunks y las comprobaciones de desarrollo del apartado 10.
- Conecta Redux DevTools en desarrollo, y las desconecta en producción.
Las claves del objeto reducer son la forma del estado. Elegirlas es una decisión de diseño: reservas, catalogo y sesion son los tres dominios de estado de cliente que salieron de la auditoría de 07-01.
Los slices provisionales
Como los slices completos son cosa de 07-04, de momento basta con esqueletos que permitan arrancar la aplicación. Así queda uno:
// src/funcionalidades/sesion/sliceSesion.js — VERSIÓN PROVISIONAL de 07-03
import { createSlice } from '@reduxjs/toolkit';
const sliceSesion = createSlice({
name: 'sesion',
initialState: { usuario: null, cargando: true },
reducers: {} // se rellena en 07-04
});
export default sliceSesion.reducer;Lo único que importa hoy: createSlice devuelve un objeto cuyo .reducer es la función que espera configureStore. Con reducers: {} el slice no acepta ninguna acción todavía, pero el almacén ya tiene la forma correcta y DevTools puede enseñártela. Los otros dos —sliceCatalogo y sliceReservas— son idénticos con su initialState correspondiente.
Comprobar que el almacén existe
Sin conectar nada a React todavía, puedes comprobarlo desde la consola del navegador o desde un fichero de prueba:
import { almacen } from './almacen/almacen.js';
console.log(almacen.getState());
// { reservas: {…}, catalogo: { tipo: 'todos', termino: '', orden: 'modelo' }, sesion: { usuario: null, cargando: true } }
almacen.dispatch({ type: 'prueba/accionInventada' });
// No falla: ningún reductor la reconoce y cada uno devuelve su estado sin cambios.
// Pero SÍ aparece en DevTools, que es lo que interesa ahora.Ese último detalle es una diferencia de fondo con el reductorReservas de 05-05: allí una acción desconocida lanzaba un error a propósito. En Redux, cada acción pasa por todos los reductores, así que una acción que un reductor no conoce es la situación normal y debe devolver el estado intacto. RTK lo hace por ti.
La API del almacén, para tenerla completa aunque en la práctica la uses poco desde componentes:
| Método | Qué hace |
|---|---|
almacen.getState() |
Devuelve el estado completo actual |
almacen.dispatch(accion) |
Envía una acción |
almacen.subscribe(fn) |
Registra un oyente que se llama tras cada acción; devuelve la función para darse de baja |
react-redux usa estos tres métodos por debajo. Desde los componentes usarás useSelector y useDispatch (07-05), no estos.
- El middleware por defecto y sus comprobaciones de desarrollo
Un middleware es una función que se interpone entre dispatch y el reductor. Es el punto único de intercepción que el contexto no tenía: por ahí pasan todas las acciones, así que ahí se registra, se mide, se transforma o se detiene.
configureStore instala tres por defecto:
| Middleware | Qué hace | Activo en |
|---|---|---|
thunk |
Permite despachar funciones además de objetos: la base de la asincronía (07-04) | Desarrollo y producción |
serializableCheck |
Avisa si metes en el estado o en una acción algo no serializable: Date, Map, Set, funciones, promesas, clases |
Solo desarrollo |
immutableCheck |
Avisa si algún reductor muta el estado en lugar de devolver uno nuevo | Solo desarrollo |
Las dos comprobaciones son la red de seguridad de los principios 2 y 3, y merecen un ejemplo cada una.
// ⚠️ Provoca el aviso de serializableCheck
despachar({ type: 'reservas/reservaCreada', payload: { fechaInicio: new Date() } });
// A non-serializable value was detected in an action, in the path: `payload.fechaInicio`
// ✅ Correcto: cadenas ISO, no objetos Date
despachar({ type: 'reservas/reservaCreada', payload: { fechaInicio: '2026-05-04T09:00' } });Por qué importa: si el estado es serializable, se puede volcar a JSON, adjuntar a un informe de fallo, persistir en localStorage y reproducir en DevTools. Un Date dentro del estado rompe las cuatro cosas. Guarda cadenas ISO —que es justo lo que hace el canon de Reserva, con fechaInicio: '2026-05-04T09:00'— y convierte a Date en el momento de formatear.
// ⚠️ Provoca el aviso de immutableCheck: muta el estado FUERA de un slice
function reductorMalo(estado, accion) {
estado.reservas.push(accion.payload); // mutación real
return estado;
}
// A state mutation was detected between dispatches, in the path: `reservas.reservas`Un aviso importante que se resuelve del todo en 07-04: dentro de un createSlice, escribir estado.reservas.push(...) es correcto y no dispara este aviso, porque Immer te entrega un borrador y no el estado real. Fuera de un slice, es una mutación de verdad. La diferencia se explica a fondo en la próxima lección.
Las dos comprobaciones cuestan tiempo: recorren el estado entero tras cada acción. Con estados grandes se nota en desarrollo, y por eso RTK las desactiva en producción automáticamente. Si en desarrollo te molestan sobre un dato concreto:
export const almacen = configureStore({
reducer: { reservas: reductorReservas, catalogo: reductorCatalogo, sesion: reductorSesion },
middleware: (obtenerPorDefecto) =>
obtenerPorDefecto({
serializableCheck: { ignoredPaths: ['reservas.controladorAborto'] }
})
});Fíjate en la firma: middleware recibe una función que devuelve la lista por defecto, y tú la ajustas. Si escribes middleware: [miMiddleware] te cargas thunk y las comprobaciones de golpe, que es un error frecuente y difícil de diagnosticar.
Añadir un middleware propio se hace concatenando:
const registro = (almacen) => (siguiente) => (accion) => {
console.groupCollapsed(accion.type);
console.log('antes:', almacen.getState());
const resultado = siguiente(accion);
console.log('después:', almacen.getState());
console.groupEnd();
return resultado;
};
export const almacen = configureStore({
reducer: { /* … */ },
middleware: (obtenerPorDefecto) => obtenerPorDefecto().concat(registro)
});Esas tres flechas anidadas son la firma canónica del middleware de Redux: recibe el almacén, devuelve una función que recibe el siguiente eslabón de la cadena y devuelve la que recibe la acción. En la práctica no escribirás muchos, pero es útil reconocer la forma. Y este ejemplo concreto es innecesario: DevTools ya te da eso mismo y mejor.
- Proveer el almacén en
main.jsx
main.jsxreact-redux expone <Provider store={…}>, que pone el almacén a disposición de todo el árbol. La pregunta interesante es dónde colocarlo respecto de lo que ya hay.
// src/main.jsx — con el almacén de Redux
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { RouterProvider } from 'react-router';
import { almacen } from './almacen/almacen.js';
import { router } from './rutas.jsx';
import LimiteDeError from './componentes/LimiteDeError.jsx';
import { Proveedores } from './contextos/Proveedores.jsx';
import { registrarError } from './utilidades/monitorizacion.js';
import './index.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<LimiteDeError titulo="CicloUrbano no está disponible ahora mismo" alRegistrar={registrarError}>
<Provider store={almacen}>
<Proveedores>
<RouterProvider router={router} />
</Proveedores>
</Provider>
</LimiteDeError>
</StrictMode>
);flowchart TD
A["StrictMode"] --> B["LimiteDeError"]
B --> C["Provider store={almacen}"]
C --> D["Proveedores<br/>Tema + Usuario"]
D --> E["RouterProvider"]
E --> F["Diseno · rutas"]
Justificación del orden, porque cada nivel responde a un motivo:
LimiteDeErrorfuera de todo (04-05): debe capturar también los fallos que ocurran al crear el almacén o al inicializar los proveedores.Providerpor encima deProveedoresy del enrutador. Es lo más importante:useSelectorsolo funciona dentro delProvider, así que cualquier componente que llegue a usar Redux —incluidos los de las páginas de error de ruta— debe estar dentro. Y como el almacén es un objeto externo que nunca cambia de identidad, colocarlo arriba del todo no tiene coste:<Provider>no vuelve a publicar nada.Proveedoressigue existiendo. Redux no sustituye al contexto: el tema seguirá enContextoTema(07-01 explicó por qué). Y si un proveedor necesitara leer del almacén, tendría que estar por dentro delProvider, que es el caso.RouterProviderel último, como en 06-02: los proveedores que deben sobrevivir a todo, incluido elerrorElementde la ruta raíz, van por fuera.
Un detalle sobre StrictMode: en desarrollo hace que los componentes se rendericen dos veces para detectar efectos impuros. Con Redux esto no duplica acciones despachadas desde manejadores de eventos, pero sí puede duplicar las despachadas desde un useEffect sin dependencias correctas. Si en DevTools ves cada acción de carga por duplicado en desarrollo, esa es casi siempre la causa, y suele ser una señal de que el efecto está mal escrito.
- Redux DevTools: el argumento más fuerte
Ya tienes almacén y aún no consume nada. Aun así, hoy puedes ver la herramienta que justifica media lección.
Instalación: la extensión Redux DevTools para el navegador (Chrome, Firefox o Edge). No hay que configurar nada más: configureStore la conecta sola en desarrollo. Arranca npm run dev, abre la aplicación, abre las herramientas del navegador y busca la pestaña Redux.
Qué te enseña:
| Panel | Qué muestra |
|---|---|
| Lista de acciones | Todas las acciones despachadas, en orden, con su type |
| Action | La acción seleccionada completa, con su payload |
| State | El estado completo justo después de esa acción, como árbol navegable |
| Diff | Solo lo que cambió con esa acción. El panel más útil de todos |
| Trace | La pila de llamadas desde donde se despachó, si está activada |
Y lo que se puede hacer, no solo mirar:
- Recorrer el historial. Pulsando una acción anterior, la aplicación vuelve al estado que tenía en ese instante. La interfaz se actualiza de verdad.
- Deslizador de reproducción. Reproduce la sesión entera como una película, acción a acción.
- Saltar acciones (skip). Desactiva una acción del historial y recalcula el estado sin ella: «¿qué habría pasado si esta acción no se hubiera despachado?».
- Despachar acciones a mano. Escribir una acción y enviarla al almacén para probar una transición sin tocar la interfaz.
- Exportar e importar. Guardar el historial en un fichero JSON y cargarlo en otra máquina. Un informe de fallo deja de ser «no me funciona» y pasa a ser un fichero que reproduce el problema exacto.
flowchart LR
A["sesion/sesionIniciada"] --> B["catalogo/tipoCambiado"]
B --> C["reservas/reservaCreada"]
C --> D["reservas/reservaConfirmada"]
D -. "saltar al estado tras B" .-> B
Esto es lo que el contexto no puede darte y por lo que muchos equipos eligen Redux. Y es un argumento honesto: cuando el fallo es «a veces, al confirmar dos reservas seguidas, la segunda aparece cancelada», tener la lista exacta de acciones y el diff de cada una convierte una tarde de depuración en cinco minutos.
Nada de esto funciona en producción, y es deliberado: las DevTools se desconectan en la compilación de producción para no exponer el estado ni el historial de un usuario real.
- Cuándo NO usar Redux
Un apartado imprescindible, porque la mitad de los problemas con Redux vienen de usarlo donde no tocaba.
No uses Redux si:
| Situación | Qué usar en su lugar |
|---|---|
| Tu «estado global» son en realidad datos de una API | TanStack Query o RTK Query (07-06) |
| Solo necesitas evitar props de paso para sesión, tema o idioma | Contexto (07-02) |
| El estado es local de un componente | useState |
| El estado es complejo pero de un solo dominio y un subárbol | useReducer + contexto |
| El dato debe poder compartirse por enlace | La URL, con useSearchParams (06-02) |
| Es un proyecto pequeño de una o dos personas | Contexto, o Zustand si quieres un almacén sin ceremonia |
| Estás empezando y aún no sabes qué estado tendrás | Empieza con useState y sube según haga falta |
Sí es una buena elección cuando se cumplen varias de estas a la vez: la aplicación es grande y va a crecer; hay varias personas tocando el mismo estado y hace falta una convención; la lógica de estado tiene reglas de negocio que merece la pena auditar y probar; necesitas depurar fallos difíciles de reproducir; o quieres interceptar cambios en un único punto para registrar, persistir o medir.
En CicloUrbano, honestamente: una aplicación de este tamaño funcionaría perfectamente con contexto + useReducer, y así ha funcionado hasta ahora. Se introduce Redux porque es lo que te vas a encontrar en el trabajo, porque el modelo mental se transfiere a Zustand y a Jotai, y porque las DevTools son una herramienta que conviene tener en las manos al menos una vez. Lo que no vamos a hacer es fingir que era imprescindible.
Errores Comunes y Consejos
Error 1: instalar redux y redux-thunk además de RTK. Vienen dentro. Instalarlos aparte puede meter dos copias de Redux en el paquete final y provocar fallos incomprensibles de «dos almacenes distintos».
Error 2: sustituir el array de middleware en vez de concatenar. middleware: [miMiddleware] elimina thunk y las dos comprobaciones. La forma correcta es siempre (obtenerPorDefecto) => obtenerPorDefecto().concat(miMiddleware).
Error 3: guardar objetos Date, Map, Set o instancias de clase en el estado. Rompen la serializabilidad, y con ella el viaje en el tiempo, la exportación del historial y la persistencia. Cadenas ISO y objetos planos.
Error 4: colocar el Provider dentro del RouterProvider. Cualquier componente fuera de él —empezando por el errorElement de la ruta raíz— fallará al usar useSelector con un error poco descriptivo. El almacén va arriba.
Error 5: crear más de un almacén. Redux está diseñado para uno solo por aplicación. Si sientes la necesidad de crear un segundo, lo que quieres es otro slice.
Error 6: escribir el default de un reductor lanzando un error, por analogía con el reductorReservas de 05-05. En Redux cada acción pasa por todos los reductores, así que lanzar rompería la aplicación en la primera acción de otro dominio.
Consejo 1: crea el almacén antes que los slices. Empieza con esqueletos vacíos, arranca la aplicación, abre DevTools y comprueba que ves el estado inicial. Así el primer slice real se escribe con el andamiaje ya verificado.
Consejo 2: usa la extensión desde el primer día. No la instales cuando tengas un fallo: para entonces habrás perdido el historial de cómo llegaste hasta él.
Consejo 3: exporta el almacén como constante nombrada (export const almacen), no por defecto. Es un objeto único de la aplicación y conviene que se llame igual en todos los ficheros.
Ejercicios
Ejercicio 1. Crea el almacén de CicloUrbano desde cero: instala los paquetes, escribe los tres slices provisionales con el estado inicial que corresponda a cada dominio según la auditoría de 07-01, monta src/almacen/almacen.js, coloca el Provider en main.jsx y comprueba en Redux DevTools que el estado inicial tiene la forma esperada. Escribe el estado inicial que has elegido para cada uno y justifícalo.
Ejercicio 2. Este configureStore tiene tres problemas. Encuéntralos y corrígelo.
import { configureStore } from '@reduxjs/toolkit';
import thunk from 'redux-thunk';
import { registro } from './middlewares/registro.js';
export const almacen = configureStore({
reducer: reductorReservas,
middleware: [thunk, registro],
preloadedState: {
sesion: { usuario: { id: 'usr-01' }, entradaEn: new Date() }
}
});Ejercicio 3. Para cada uno de estos cinco datos de una futura ampliación de CicloUrbano, decide si va al almacén de Redux o no, y justifícalo con el árbol de decisión de 07-01 y con el apartado 13.
- La lista de incidencias abiertas del taller, que llega de
GET /incidencias. - El idioma de la interfaz, elegido por el usuario y usado por toda la aplicación.
- La posición del deslizador de precio máximo del catálogo, mientras el usuario lo arrastra.
- El historial de las últimas cinco bicicletas consultadas, que se muestra en la cabecera y persiste entre sesiones.
- Si el
DialogoReservaestá abierto.
Soluciones
Solución 1.
// src/funcionalidades/catalogo/sliceCatalogo.js — provisional
import { createSlice } from '@reduxjs/toolkit';
const sliceCatalogo = createSlice({
name: 'catalogo',
initialState: {
tipo: 'todos', // 'todos' | 'urbana' | 'electrica' | 'carga'
termino: '', // texto del buscador
orden: 'modelo' // 'modelo' | 'precio'
},
reducers: {}
});
export default sliceCatalogo.reducer;// src/funcionalidades/reservas/sliceReservas.js — provisional
import { createSlice } from '@reduxjs/toolkit';
const sliceReservas = createSlice({
name: 'reservas',
initialState: {
entidades: {}, // reservas por id
ids: [], // orden de aparición
estadoCarga: 'inactivo',// 'inactivo' | 'cargando' | 'correcto' | 'error'
error: null
},
reducers: {}
});
export default sliceReservas.reducer;// src/funcionalidades/sesion/sliceSesion.js — provisional
import { createSlice } from '@reduxjs/toolkit';
const sliceSesion = createSlice({
name: 'sesion',
initialState: { usuario: null, cargando: true },
reducers: {}
});
export default sliceSesion.reducer;Justificación de cada estado inicial:
catalogo: los tres criterios de filtrado, todos con un valor neutro.tipoempieza en'todos'para coincidir con lo que ya hacíaPaginaCatalogocuando no hay?tipo=en la URL. Ojo: que existatipoaquí no significa que el filtro deje de vivir en la URL; la relación entre ambos se resuelve en 07-05.reservas: forma normalizada (entidades+ids), porque las reservas se buscan por identificador constantemente. Se detalla en 07-04. Más un parestadoCarga/errorpara la carga asíncrona, con'inactivo'porque aún no se ha pedido nada.sesion:usuario: nullporque nadie ha entrado, ycargando: trueporque al arrancar hay que comprobar si existe una sesión guardada —exactamente elcargandoSesionde 06-05—. Empezar enfalseprovocaría queRutaProtegidaexpulse a un usuario con sesión válida durante el primer render.
En DevTools, con el almacén montado, el panel State debe mostrar las tres claves y ninguna acción salvo la de inicialización de Redux (@@INIT).
Solución 2. Los tres problemas:
reducer: reductorReservaspasa un único reductor como reductor raíz. El estado sería directamente el de reservas, sin las clavesreservas,catalogoysesion. Debe ser un objeto con el mapa de reductores.middleware: [thunk, registro]sustituye la lista por defecto, así que se pierdenserializableCheckeimmutableCheck. Ythunkestá de más: RTK ya lo incluye, y la líneaimport thunk from 'redux-thunk'sobra por completo.entradaEn: new Date()mete un objetoDateno serializable en el estado precargado. Debe ser una cadena ISO.
import { configureStore } from '@reduxjs/toolkit';
import { registro } from './middlewares/registro.js';
import reductorReservas from '../funcionalidades/reservas/sliceReservas.js';
import reductorCatalogo from '../funcionalidades/catalogo/sliceCatalogo.js';
import reductorSesion from '../funcionalidades/sesion/sliceSesion.js';
export const almacen = configureStore({
reducer: {
reservas: reductorReservas,
catalogo: reductorCatalogo,
sesion: reductorSesion
},
middleware: (obtenerPorDefecto) => obtenerPorDefecto().concat(registro),
preloadedState: {
sesion: { usuario: { id: 'usr-01' }, entradaEn: '2026-05-04T09:00:00.000Z', cargando: false }
}
});preloadedState sirve para restaurar un estado guardado o para fijar un punto de partida en las pruebas, y debe respetar la misma forma que el mapa de reductores.
Solución 3.
| Dato | ¿A Redux? | Justificación |
|---|---|---|
| 1. Incidencias del taller | No | Es estado del servidor. La primera pregunta del árbol de 07-01 se responde afirmativamente y ahí termina el recorrido: va a una caché de consultas (07-06). Meterlo en un slice obliga a escribir a mano carga, error, cancelación, revalidación e invalidación |
| 2. Idioma de la interfaz | No hace falta | Muchos lectores, cambio poquísimo frecuente: el perfil exacto del contexto, como el tema. Si el proyecto ya usa Redux para otras cosas, ponerlo en un slicePreferencias tampoco es un error; simplemente no aporta nada |
| 3. Deslizador de precio, mientras se arrastra | No | Cambia decenas de veces por segundo. Cada movimiento sería una acción en el historial de DevTools, que quedaría inservible, y un ciclo de comparación para todos los suscriptores. Estado local con useDebounce; y cuando el usuario suelte, el valor definitivo puede ir a la URL junto a ?tipo= |
| 4. Historial de bicicletas consultadas | Sí | Es estado de cliente genuino: lo lee un componente lejano (la cabecera), lo escribe otro (PaginaFichaBicicleta), tiene una regla propia («las últimas cinco, sin repetidas») que merece un reductor y una prueba, y su persistencia se resuelve limpiamente con un middleware que escribe en localStorage tras cada acción |
5. DialogoReserva abierto |
No | Estado local de interfaz, el ejemplo canónico del apartado 13 y de la tabla de 07-01. Va en un useAlternar dentro del componente que lo abre |
El caso 4 es el más interesante porque es el único que gana de verdad con Redux, y por un motivo que no es «lo usan varios componentes» —eso también lo cubriría el contexto— sino la combinación de tener una regla de negocio propia y necesitar persistencia en un punto único: exactamente las dos cosas que el apartado 13 pone en la columna de «sí».
Conclusión
Redux es un contenedor de estado predecible, y lo que compras con él no es velocidad sino predecibilidad y capacidad de diagnóstico. Sus tres principios —fuente única de verdad en un único objeto, estado de solo lectura que solo cambia despachando acciones, y cambios mediante funciones puras— producen juntos una propiedad muy concreta: estado inicial más lista de acciones igual a estado actual, siempre. De ahí salen el viaje en el tiempo, la reproducción de fallos, el registro automático y unas pruebas que son llamar a una función y comparar la salida. El modelo es exactamente el useReducer de 05-05 a otra escala: las diferencias son que el estado vive fuera del árbol de React, que el campo se llama type y la carga útil payload, que hay un punto único de intercepción —el middleware— y que hay herramientas.
Has montado el almacén con Redux Toolkit, que es la forma oficial de usar Redux hoy: configureStore combina los reductores, instala thunk y las comprobaciones de desarrollo de serializabilidad y mutación, y conecta las DevTools sin que tengas que pedirlo. El almacén de CicloUrbano vive en src/almacen/almacen.js con tres dominios —reservas, catalogo y sesion—, hoy con slices provisionales, y se provee con <Provider store={almacen}> colocado por encima de Proveedores y del RouterProvider, porque useSelector solo funciona dentro y porque el almacén, al ser un objeto externo de identidad fija, no cuesta nada arriba del todo. Del Redux clásico —createStore, constantes de tipo, switch a mano, combineReducers explícito, applyMiddleware(thunk)— te llevas solo la capacidad de leerlo en proyectos antiguos; no volverá a aparecer como código propuesto. Y sabes cuándo no usar Redux, que en esta lección ocupa una tabla entera: datos de servidor, preferencias ambientales, estado local, valores que deben viajar en un enlace y proyectos pequeños tienen respuestas mejores.
Ahora el almacén está vacío. Lo que falta es lo que le da valor: modelar el estado y sus transiciones. En la próxima lección escribirás los slices de verdad con createSlice, entenderás por qué Immer te deja escribir estado.reservas.push(...) sin mutar nada, generarás identificadores y fechas fuera del reductor con prepare, normalizarás las entidades por id, declararás los selectores junto a su slice y resolverás la carga asíncrona con createAsyncThunk y sus tres estados. La próxima lección es Redux: Acciones y Reductores.
Curso de React
Módulo 1: Introducción a React
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
