configureStore is the officially recommended replacement for createStore + applyMiddleware — confirmed against the real, installed @reduxjs/toolkit (v2.12.0): it wires up devtools, thunk middleware, and two dev-only safety middlewares automatically, where mutation detection genuinely THROWS an error (not just a warning) while the serializability check only logs via console.error.
reduxredux-toolkitconfigureStorestate-management
Knowledge Check
10 questions · pass at 70%
0/10
mediummcq
1. What does configureStore's mutation-detection middleware do when it detects an actual reducer mutation, confirmed by direct testing?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
configureStore({ reducer, middleware?, devTools? })
→ replaces: createStore + combineReducers + applyMiddleware +
composeWithDevTools, all manual ceremony from earlier topics
AUTOMATIC SETUP (confirmed):
✓ Redux DevTools Extension — wired up automatically (dev)
✓ redux-thunk — included by default (enables dispatching functions)
✓ combineReducers — called internally for a plain reducer object
✓ mutation-detection middleware (DEV ONLY) → CONFIRMED: THROWS
✓ serializability-check middleware (DEV ONLY) → CONFIRMED: only
console.error, does NOT throw
reducer: { counter: counterReducer, user: userReducer }
→ same as combineReducers({ ... }) from Actions & Reducers
EXTEND (don't replace) default middleware:
middleware: (getDefault) => getDefault().concat(myMiddleware)
⚠️ createSlice's "state.x += 1" syntax does NOT trigger mutation
detection — Immer translates it into a real new object first
(see createSlice) — before !== after, confirmed.
⚠️ Both dev-only checks are STRIPPED from production builds —
"no error in dev" ≠ "guaranteed safe in production."
Best Practices named Redux Toolkit as "Strongly Recommended" — this topic covers configureStore, RTK's replacement for the hand-written createStore + applyMiddleware combination from Store and Middleware.
import { configureStore } from "@reduxjs/toolkit";import counterReducer from "./counterSlice";import userReducer from "./userSlice";const store = configureStore({ reducer: { counter: counterReducer, user: userReducer, },});
That reducer object plays the exact same role combineReducers({...}) played in Actions & Reducers — configureStore calls combineReducers internally for you when given a plain object here (you can also pass a single already-combined reducer function directly, same as createStore accepted). The resulting store still has the exact same four-method surface (getState, dispatch, subscribe, replaceReducer) — configureStore doesn't change what a Redux store is, only how conveniently and safely it's constructed.
Confirmed by inspecting the actual installed package's behavior:
Redux DevTools Extension integration — enabled automatically, with no manual wiring, in development builds.
redux-thunk middleware — included by default, which is what makes dispatching a function (not just a plain action object) work at all, the foundation createAsyncThunk builds on.
Mutation-detection middleware (development only) — actively checks whether a reducer mutated its state argument.
Serializability-check middleware (development only) — actively checks dispatched actions and resulting state for non-serializable values (functions, Promises, class instances).
// hand-written equivalent of what configureStore does automatically:const store = createStore( rootReducer, composeWithDevTools(applyMiddleware(thunk, ...otherMiddleware))); // ← configureStore replaces ALL of this ceremony with one call
The store configureStore produces still funnels through dispatch → combineReducers, exactly as before
step 1 / 4
store.dispatch({ type: 'counter/incremented' });
Action
active
{ type: 'counter/incremented' }
→
Root reducer
waiting
about to receive it
dispatch() sends the action to the store's single root reducer — this is the ONLY way state changes in Redux; nothing else can mutate the store.
Confirmed: mutation detection THROWS, serializability check only WARNS#
function badReducer(state = { items: [] }, action) { if (action.type === "add") { state.items.push(action.payload); // ❌ direct mutation return state; } return state;}const store = configureStore({ reducer: badReducer });store.dispatch({ type: "add", payload: 1 });// THROWN: "A state mutation was detected inside a dispatch, in the path: items.0. ..."
Confirmed by running this exact code against the real, installed @reduxjs/toolkit: mutation detection doesn't just log a warning — it genuinely throws an error, halting execution at the dispatch call, with a message identifying the exact mutated path (items.0). This is a meaningfully stronger guarantee than developer discipline alone; a mutation bug like this is caught immediately in development, not discovered later via a subtle stale-UI bug.
store.dispatch({ type: "set", payload: () => {} }); // dispatching a function as payload// console.error (NOT thrown): "A non-serializable value was detected in an action, in the path: `payload`..."
The serializability check, confirmed by the same kind of direct test, behaves differently — it logs via console.error but does not throw or halt execution. Both checks are development-only and are automatically stripped from production builds, so neither affects production performance.
Does this throw a mutation-detected error, given state.value += 1 looks like direct mutation?
Solution
No error — logs false 0 1.
This is the key distinction covered fully in createSlice: createSlice's reducers run through Immer, which intercepts this "mutating" syntax and translates it into a genuine immutable update behind the scenes — state.value += 1 inside a createSlice reducer does NOT produce a real mutation of the original object; Immer produces a new object with the same field updated, which is exactly why before === after is confirmed false here despite the mutating-looking syntax. configureStore's mutation-detection middleware only fires on actual mutations of the original reducer input — which is precisely why hand-written reducers (like the earlier badReducer example, not wrapped by Immer) trigger it, while createSlice-generated reducers don't.
Sketch what configureStore is doing internally, composing pieces already covered in this domain:
function myConfigureStore({ reducer, middleware }) { const rootReducer = typeof reducer === "function" ? reducer : combineReducers(reducer); // from Actions & Reducers const middlewareList = middleware ?? [thunkMiddleware]; // thunk included by default if (process.env.NODE_ENV !== "production") { middlewareList.push(mutationDetectionMiddleware, serializabilityCheckMiddleware);
Every individual piece here — combineReducers, applyMiddleware, createStore — is exactly what was covered as separate, hand-assembled concepts in Store, Actions & Reducers, and Middleware. configureStore isn't a different underlying system; it's those same pieces, assembled with good defaults, plus two extra dev-only safety middlewares layered in automatically.
This topic is the direct payoff of everything built up across Store through Best Practices — configureStore produces exactly the same kind of store object those topics described, just constructed with less ceremony and two extra dev-only safety nets. The mutation-detection behavior confirmed here directly enforces the Essential-tier "never mutate state" rule from Best Practices mechanically, rather than relying purely on code review.
configureStore's default middleware array already includes thunk — use the getDefaultMiddleware() callback form ((getDefault) => getDefault().concat(myOtherMiddleware)) to extend the defaults, not manually re-list ones already present.
2. Assuming configureStore's dev-only checks run in production#
// Believing mutation bugs will be caught in production the same way they are in dev
Both the mutation-detection and serializability-check middlewares are stripped from production builds for performance — they're development safety nets, not a production-time guarantee. A mutation bug that slipped past development testing is not automatically caught in production.
3. Passing a single already-combined reducer when a per-slice object would be clearer#
configureStore calls combineReducers internally when given a plain object of slice reducers — pre-combining yourself just adds an unnecessary manual step; pass { counter: counterReducer, user: userReducer } directly.
Use configureStore for all new Redux code — it's the officially recommended entry point, and its dev-only mutation/serializability checks catch real Essential-tier rule violations automatically.
Extend, don't replace, the default middleware array — use the getDefaultMiddleware() callback form to keep DevTools integration, thunk, and the dev-only safety middlewares while adding your own.
Remember dev-only checks are dev-only — don't treat "no mutation error in development" as proof a reducer is bug-free in all cases; the same discipline (no mutation, ever) still matters.
The dev-only mutation and serializability checks do real, non-trivial work (deep comparisons) on every single dispatched action — this is an intentional, accepted development-time cost; it disappears entirely in production builds, so it's not a production performance concern.
configureStore's automatic DevTools integration itself has a small overhead (serializing state for the DevTools panel) that's also development-only.