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:
- Redux DevTools Extension integration, enabled automatically, with no manual wiring, in development builds.
redux-thunkmiddleware, 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 callstore.dispatch({ type: 'counter/incremented' });
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().counter;
store.dispatch(counterSlice.actions.incremented());
const after = store.getState().counter;
console.log(before === after, before.value, after.value);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); // dev-only extras
}
const enhancer =
process.env.NODE_ENV !== "production" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(applyMiddleware(
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 devBoth 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 redundantconfigureStore 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
configureStorefor 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.
