wandres.dev
REDUX TOOLKIT · slices y RTK Query

configureStore: el store con buenos defaults

configureStore reemplaza el createStore del Redux clásico y su cableado manual de middleware y DevTools por una sola llamada con defaults sensatos: combina los reducers, instala redux-thunk, activa Redux DevTools y monta en desarrollo tres guardianes que detectan mutaciones, valores no serializables y errores de despacho. Esta lección explica cada default, por qué las comprobaciones solo corren en desarrollo, cómo ajustar el middleware con getDefaultMiddleware sin perder el orden correcto, cómo cablear el listenerMiddleware para efectos reactivos, y cómo inferir los tipos RootState y AppDispatch para tipar los hooks con withTypes en React-Redux 9.

⏱ 16 min

El createStore del Redux clásico era honesto pero desnudo: te daba un store vacío y dejaba en tus manos ensamblar todo lo demás, applyMiddleware para el thunk, composeWithDevTools para la inspección, combineReducers para juntar slices, y acertar el orden de esa composición o sufrir bugs sutiles. configureStore invierte la carga de la prueba: en vez de partir de la nada y añadir, parte de una configuración experta y te deja quitar o ajustar. Con una llamada obtienes un store que combina tus reducers, ejecuta thunks, se conecta a las DevTools y monta en desarrollo un cuerpo de guardianes que atrapan los errores más comunes de Redux antes de que lleguen a producción. No es azúcar: es una década de experiencia depurando Redux, condensada en los valores por defecto.

🎯 Al terminar esta lección sabrás
  • Enumerar los defaults de configureStore: combinación de reducers, thunk, DevTools y comprobaciones de desarrollo.
  • Explicar qué detecta cada guardián de desarrollo y por qué se desactivan en producción.
  • Ajustar el middleware con getDefaultMiddleware respetando el orden y añadir el listenerMiddleware.
  • Inferir RootState y AppDispatch y tipar los hooks con withTypes en React-Redux 9.

Un store bien equipado sin cablearlo

configureStore acepta un mapa de reducers y hace por debajo el combineReducers que antes escribías tú: cada clave se convierte en una rama del árbol de estado. Encima, y sin que digas nada, instala redux-thunk para que puedas despachar funciones y thunks, conecta la extensión Redux DevTools con su time-travel, y en modo desarrollo activa las comprobaciones de inmutabilidad y serializabilidad. La misma llamada que monta el store te devuelve el material para inferir sus tipos, que es lo que hace tipable el resto de la app.

import { configureStore } from "@reduxjs/toolkit";
import contador from "./contadorSlice";
import sesion from "./sesionSlice";

export const store = configureStore({
  reducer: { contador, sesion },
  // devTools, thunk y comprobaciones de desarrollo ya vienen puestos
});

// tipos inferidos del store real: nunca se desincronizan del estado
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Con esos dos tipos, los hooks de React-Redux 9 se tipan una vez y para siempre con withTypes, y a partir de ahí useAppSelector conoce la forma del estado y useAppDispatch acepta thunks sin quejarse.

import { useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";

export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();

Lo que ya no cableas

El contraste con el createStore clásico deja ver cuánto andamiaje desaparece. Antes ensamblabas a mano el reducer combinado, el middleware y la conexión a DevTools, y un orden equivocado o un compose mal escrito te costaba una tarde de depuración:

// Redux clasico: cableado manual y fragil, esto es lo que ya NO escribes
import { createStore, applyMiddleware, combineReducers, compose } from "redux";
import thunk from "redux-thunk";

const raiz = combineReducers({ contador, sesion });
const componer =
  (globalThis as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?? compose;
const store = createStore(raiz, componer(applyMiddleware(thunk)));

Todo eso lo resuelve una sola llamada a configureStore, que además incorpora las comprobaciones de desarrollo que el montaje manual jamás traía. La diferencia no es solo teclear menos: es no poder equivocarte en el orden ni olvidar una pieza.

Los guardianes de desarrollo

Los defaults más valiosos no son comodidades sino alarmas. configureStore monta tres middlewares de comprobación que solo corren en desarrollo, porque su coste es real y en producción sobra. El de inmutabilidad congela el estado y avisa si algo lo muta fuera de un reducer de Immer. El de serializabilidad recorre acciones y estado buscando valores que Redux no debería contener —promesas, funciones, instancias de clase, Map, Set— porque romperían la persistencia, el time-travel y la reproducibilidad. El de action creators avisa si despachas un action creator sin llamarlo. Son opiniones convertidas en runtime: para RTK, mutar el estado y guardar valores no serializables no son estilos discutibles, son bugs.

🧊

Inmutabilidad

Detecta mutaciones del estado fuera de un reducer. Congela el árbol en desarrollo y grita si alguien lo escribe por la espalda, el error más traicionero de Redux.

📦

Serializabilidad

Recorre acciones y estado buscando valores no serializables. Los prohíbe porque romperían la persistencia, el time-travel y la reproducibilidad del historial.

📣

Action creators

Avisa si despachas un action creator sin invocarlo. Un olvido de paréntesis que antes fallaba en silencio ahora salta al instante.

ℹ️
Solo en desarrollo, y por qué

Las tres comprobaciones recorren estructuras completas en cada acción, así que en aplicaciones con estado grande cuestan tiempo. Por eso RTK las activa solo cuando process.env.NODE_ENV no es production: en desarrollo pagas ese coste a cambio de cazar bugs temprano, y en producción desaparecen sin dejar rastro. Si una acción legítima transporta algo no serializable —el identificador opaco de una conexión, por ejemplo—, no desactives el guardián entero: exímelo con precisión mediante ignoredActions e ignoredPaths, y mantén la red de seguridad para todo lo demás.

Middleware y efectos reactivos

Cuando necesites tocar la cadena de middleware, configureStore te obliga a hacerlo bien: el campo middleware recibe una función que te entrega getDefaultMiddleware, y tú devuelves esa base ajustada. Así nunca borras por accidente el thunk ni las comprobaciones; los conservas y añades encima. El orden importa —lo que va delante ve la acción antes—, y por eso existen prepend y concat. El añadido más útil de RTK moderno es el listenerMiddleware: la respuesta oficial y ligera para efectos reactivos, que en la mayoría de los casos jubila a las sagas y a los observables. Escuchas una acción y disparas un efecto, con cancelación incluida.

import { configureStore, createListenerMiddleware } from "@reduxjs/toolkit";
import { login } from "./sesionSlice";
import { cargarPreferencias } from "./prefsThunks";

export const escucha = createListenerMiddleware();

escucha.startListening({
  actionCreator: login.fulfilled,
  effect: async (accion, api) => {
    api.cancelActiveListeners(); // estilo takeLatest: cancela ejecuciones previas
    await api.delay(300);
    api.dispatch(cargarPreferencias(accion.payload.id));
  },
});

export const store = configureStore({
  reducer: raiz,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ["socket/mensajeRecibido"],
        ignoredPaths: ["socket.conexion"],
      },
    }).prepend(escucha.middleware), // el listener ve las acciones primero
});
📝
prepend ve antes, concat ve después

El orden del pipeline importa porque cada middleware decide si deja pasar la acción al siguiente. prepend coloca el tuyo al principio, así que ve las acciones antes que el thunk y las comprobaciones; concat lo pone al final. Un listenerMiddleware suele ir con prepend para reaccionar cuanto antes, mientras que el middleware de RTK Query va con concat porque opera sobre acciones ya procesadas. getDefaultMiddleware devuelve un array tipado, y tanto prepend como concat preservan esos tipos para que AppDispatch siga sabiendo qué thunks acepta.

flowchart LR
A[dispatch de una accion] --> B[listenerMiddleware]
B --> C[thunk]
C --> D[comprobaciones de desarrollo]
D --> E[reducer raiz]
E --> F[estado nuevo]
F --> G[Redux DevTools]
style D fill:#f9e2af,color:#11111b
style E fill:#a6e3a1,color:#11111b
style G fill:#cba6f7,color:#11111b
💡
combineSlices e inyección diferida para code splitting

En apps grandes no quieres cargar todos los reducers en el arranque. combineSlices construye el reducer raíz con un método inject que añade slices en caliente, de modo que el módulo de una ruta puede inyectar su propio slice al cargarse por primera vez. Combinado con injectEndpoints de RTK Query, permite que cada porción del estado viaje en el mismo trozo de código que la vista que la usa, en lugar de inflar el bundle inicial con estado que quizá nunca se visite.

Los buenos defaults son experiencia acumulada, no comodidad

Es tentador leer configureStore como una conveniencia —una forma de teclear menos que createStore más applyMiddleware más composeWithDevTools— y quedarse con eso es perderse lo esencial. Un default no es neutro: es una decisión tomada de antemano por alguien, y la pregunta que revela su valor es quién la tomó y con cuánto conocimiento. Los defaults del Redux clásico los tomabas tú, en cada proyecto, a menudo copiando de un tutorial sin entender el orden del middleware ni por qué el thunk iba antes que el logger. Los defaults de configureStore los toma el equipo que mantiene Redux, con la memoria de todos los bugs que el ecosistema reportó durante una década: mutaciones accidentales que corrompían el time-travel, valores no serializables que rompían la persistencia, DevTools mal conectadas, órdenes de middleware que tragaban acciones. Cada guardián que se activa en desarrollo es la cicatriz de una clase entera de errores que alguien sufrió para que tú no tengas que sufrirla. Por eso desactivar una comprobación sin entenderla no es simplificar, es renunciar a conocimiento ajeno más caro que el tuyo; y por eso el patrón correcto para ajustar el middleware es partir de getDefaultMiddleware y modificar, nunca reconstruir desde cero, porque reconstruir es presumir que sabes más que la suma de todos los que depuraron Redux antes que tú. Esta es la forma madura de usar una herramienta con opiniones: no obedecer sus defaults por pereza ni ignorarlos por soberbia, sino entender qué error concreto previene cada uno, de modo que cuando de verdad necesites apartarte de uno lo hagas con la precisión de ignoredPaths y no con el hacha de desactivarlo entero.

⚔️ Interroga los defaults de tu store
  1. Monta un store con configureStore e infiere RootState y AppDispatch; tipa useAppSelector y useAppDispatch con withTypes y comprueba que un selector mal tipado ahora falla en compilación.
  2. Muta el estado a propósito fuera de un reducer y observa cómo el guardián de inmutabilidad lo caza en desarrollo; luego arréglalo.
  3. Despacha una acción con un valor no serializable, lee el aviso del guardián de serializabilidad y exímelo con ignoredPaths en lugar de desactivar la comprobación entera.
  4. Añade un listenerMiddleware que reaccione a una acción y dispare un efecto con delay; usa cancelActiveListeners y demuestra el comportamiento estilo takeLatest.
  5. Compara en las DevTools el time-travel antes y después de introducir un valor no serializable, y argumenta por qué el guardián existe.
  6. Divide un slice pesado con combineSlices e inject y verifica en la pestaña de red que su código no viaja en el bundle inicial.