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

  1. Qué es Redux y qué problema resuelve
  2. Los tres principios
  3. Por qué esos principios hacen la aplicación predecible y depurable
  4. Relación con useReducer: mismo modelo, distinta escala
  5. El flujo unidireccional
  6. Redux Toolkit: la forma oficial de usar Redux hoy
  7. Cómo era el Redux clásico (código heredado que hay que saber leer)
  8. Instalación
  9. El almacén de CicloUrbano: src/almacen/almacen.js
  10. El middleware por defecto y sus comprobaciones de desarrollo
  11. Proveer el almacén en main.jsx
  12. Redux DevTools: el argumento más fuerte
  13. Cuándo NO usar Redux

  1. 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.

  1. 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.

almacen.dispatch({ type: 'reservas/reservaConfirmada', payload: 'res-01' });

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.

  1. 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.

  1. Relación con useReducer: mismo modelo, distinta escala

Esto 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.

  1. 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:

  1. El usuario pulsa «Confirmar» en PanelReservas y el manejador despacha { type: 'reservas/reservaConfirmada', payload: 'res-01' }.
  2. 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.
  3. El reductor devuelve un estado nuevo; nunca modifica el que recibió.
  4. El almacén guarda el estado nuevo y notifica a sus suscriptores.
  5. 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.

  1. 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.

  1. 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: createSlice genera el tipo a partir del nombre.
  • El switch con default: return estado es obligatorio en Redux clásico, porque cada acción pasa por todos los reductores. Ojo con la diferencia respecto del reductorReservas de 05-05, donde el default lanzaba 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.
  • combineReducers explícito y applyMiddleware(thunk): los dos desaparecen con configureStore, que hace lo mismo por dentro.
  • Es habitual encontrarlo junto a connect, mapStateToProps y mapDispatchToProps, 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.

  1. Instalación

npm install @reduxjs/toolkit react-redux

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.

  1. El almacén de CicloUrbano: src/almacen/almacen.js

configureStore 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:

  1. Combina los reductores. El objeto que pasas en reducer se convierte internamente en un combineReducers, así que estado.reservas es lo que devuelva reductorReservas.
  2. Configura el middleware por defecto, incluido el necesario para thunks y las comprobaciones de desarrollo del apartado 10.
  3. 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.

  1. 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.

  1. Proveer el almacén en main.jsx

react-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:

  • LimiteDeError fuera de todo (04-05): debe capturar también los fallos que ocurran al crear el almacén o al inicializar los proveedores.
  • Provider por encima de Proveedores y del enrutador. Es lo más importante: useSelector solo funciona dentro del Provider, 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.
  • Proveedores sigue existiendo. Redux no sustituye al contexto: el tema seguirá en ContextoTema (07-01 explicó por qué). Y si un proveedor necesitara leer del almacén, tendría que estar por dentro del Provider, que es el caso.
  • RouterProvider el último, como en 06-02: los proveedores que deben sobrevivir a todo, incluido el errorElement de 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.

  1. 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.

  1. 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.

  1. La lista de incidencias abiertas del taller, que llega de GET /incidencias.
  2. El idioma de la interfaz, elegido por el usuario y usado por toda la aplicación.
  3. La posición del deslizador de precio máximo del catálogo, mientras el usuario lo arrastra.
  4. El historial de las últimas cinco bicicletas consultadas, que se muestra en la cabecera y persiste entre sesiones.
  5. Si el DialogoReserva está abierto.

Soluciones

Solución 1.

npm install @reduxjs/toolkit react-redux
// 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. tipo empieza en 'todos' para coincidir con lo que ya hacía PaginaCatalogo cuando no hay ?tipo= en la URL. Ojo: que exista tipo aquí 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 par estadoCarga/error para la carga asíncrona, con 'inactivo' porque aún no se ha pedido nada.
  • sesion: usuario: null porque nadie ha entrado, y cargando: true porque al arrancar hay que comprobar si existe una sesión guardada —exactamente el cargandoSesion de 06-05—. Empezar en false provocaría que RutaProtegida expulse 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:

  1. reducer: reductorReservas pasa un único reductor como reductor raíz. El estado sería directamente el de reservas, sin las claves reservas, catalogo y sesion. Debe ser un objeto con el mapa de reductores.
  2. middleware: [thunk, registro] sustituye la lista por defecto, así que se pierden serializableCheck e immutableCheck. Y thunk está de más: RTK ya lo incluye, y la línea import thunk from 'redux-thunk' sobra por completo.
  3. entradaEn: new Date() mete un objeto Date no 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 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

Módulo 2: Componentes de React

Módulo 3: Trabajando con Eventos

Módulo 4: Conceptos Avanzados de Componentes

Módulo 5: Hooks de React

Módulo 6: Enrutamiento en React

Módulo 7: Gestión del Estado

Módulo 8: Optimización del Rendimiento

Módulo 9: Pruebas en React

Módulo 10: Temas Avanzados

Módulo 11: Proyecto: Construyendo una Aplicación Completa

© Copyright 2026. Todos los derechos reservados