DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/Redux Ecosystem/configureStore
beginner15 min read·Updated Jun 2025

configureStore

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

Related topics

createSliceBest PracticesPro

On this page

15m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

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.

What configureStore sets up automatically#

Confirmed by inspecting the actual installed package's behavior:

  1. Redux DevTools Extension integration — enabled automatically, with no manual wiring, in development builds.
  2. 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.
  3. Mutation-detection middleware (development only) — actively checks whether a reducer mutated its state argument.
  4. 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.


Try It#

Predict the outcome before checking the solution.

const counterSlice = createSlice({
  name: "counter",
  initialState: { value: 0 },
  reducers: {
    incremented(state) {
      state.value += 1; // "mutating" syntax — covered fully in createSlice
    },
  },
});
 
const store = configureStore({ reducer: { counter: counterSlice.reducer } });
const before = store.getState



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.


Implement It Yourself#

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.


Under the Hood#

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.


Common Mistakes#

1. Manually re-adding middleware that's already included by default#

configureStore({
  reducer: rootReducer,
  middleware: (getDefault) => [thunkMiddleware, ...getDefault()], // ❌ thunk added TWICE
});

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({ reducer: combineReducers({ counter: counterReducer, user: userReducer }) }); // works, but redundant

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.


Best Practices#

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

Performance Tips#

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

().counter;
store.dispatch(counterSlice.actions.incremented());
const after = store.getState().counter;
console.log(before === after, before.value, after.value);
// dev-only extras
}
const enhancer =
process.env.NODE_ENV !== "production" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(applyMiddleware(...middlewareList)) // from Middleware
: applyMiddleware(...middlewareList);
return createStore(rootReducer, enhancer); // from Store
}