Redux keeps an entire app's state in ONE plain object, held by a single store, and the ONLY way to change it is dispatching a plain-object action through the store's reducer — confirmed against the real, currently-installed redux package (v5.0.1), where createStore, getState, dispatch, and subscribe all still work exactly as documented, no deprecation involved.
reduxstoresingle-source-of-truthstate-management
Knowledge Check
12 questions · pass at 70%
0/12
easymcq
1. How many stores does a typical Redux application have?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
createStore(reducer, [preloadedState])
→ CONFIRMED still functional in redux v5.0.1
→ configureStore (Redux Toolkit) is the RECOMMENDED entry point now,
but produces the same underlying store shape/API
STORE SURFACE (exactly four methods):
getState() → read current state, synchronous
dispatch(action) → the ONLY way to change state
subscribe(fn) → fn() called after EVERY dispatch (no args, no filtering)
replaceReducer(fn) → swaps root reducer at runtime (rare in app code)
SINGLE SOURCE OF TRUTH:
ONE store, ONE state object, for the WHOLE app
Features get SLICES of the tree (via combineReducers),
never their own separate store
CONFIRMED: subscribe fires on EVERY dispatch, even ones whose
reducer returns the SAME state reference unchanged (default case).
Raw subscribe does NOT filter by relevance — useSelector does.
⚠️ NEVER mutate state in the reducer — always return a NEW reference.
Mutation + same reference = invisible to DevTools AND react-redux.
Redux's entire model rests on one idea: all of an app's state lives in a single plain JavaScript object, held by a single store. Not one store per feature, not one store per component tree — one store, one object, for the whole app.
Confirmed by running this exact code against the currently-installed redux package (v5.0.1): createStore, store.getState(), store.dispatch(), and store.subscribe() all work precisely as documented — none of this API has been removed, despite Redux Toolkit's configureStore (covered in configureStore) now being the officially recommended way to create a store. The underlying store object these functions produce is the same either way.
dispatch() → reducer → store updates → subscribers notified
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.
A Redux store, no matter how it was created, exposes exactly this surface:
getState() — returns the current state object. Synchronous, always up to date.
dispatch(action) — the only way to trigger a state change. Takes a plain object (or, with middleware, other shapes — see Middleware).
subscribe(listener) — registers a callback that fires after every dispatched action that produces a state change, returning an unsubscribe function.
replaceReducer(nextReducer) — swaps the root reducer at runtime (used for code-splitting reducers; rarely needed directly in app code).
There is no fifth way to change state. No direct mutation, no setter methods — dispatch is the single funnel every state change passes through, which is precisely what makes Redux state changes traceable: every single one corresponds to exactly one dispatched action.
In a real app the store's state object is a single tree holding every feature's data — counters, user info, cart contents, UI flags, all of it. Individual features don't get their own isolated stores; they get their own slice of the one store's state tree (mechanically enforced via combineReducers, shown in the visualizer above and covered fully in Actions & Reducers). This is a deliberate design choice: a single object means the entire app's state can be inspected, logged, serialized, or time-traveled through as one coherent snapshot — which is exactly what tools like Redux DevTools rely on.
Zustand also keeps state outside React in a single external object — but Zustand's set() can be called directly from anywhere with a partial state update. Redux deliberately removes that direct-set path: the only entry point is dispatch(action), and the reducer is the only code allowed to decide what the new state looks like. The extra ceremony (defining action types, writing a reducer function) buys traceability — every state change is a named, inspectable event, not an arbitrary function call.
subscribe's listener fires after every dispatched action — including "unknown/action", even though the reducer's default case returns the exact same state reference unchanged. Redux doesn't skip notifying subscribers just because a particular action didn't produce a different state; it notifies on every dispatch, full stop. (Whether a connected component actually re-renders from that notification is a separate, later concern — covered in Selectors & Reselect — but the raw store-level subscribe callback fires three times here regardless.) The state itself only genuinely changes on the two "increment" dispatches, landing at { count: 2 }.
Build a minimal version of createStore, to see the actual mechanism Redux wraps:
function createMiniStore(reducer, preloadedState) { let state = preloadedState !== undefined ? preloadedState : reducer(undefined, { type: "@@INIT" }); // reducer's default param supplies initial state const listeners = new Set(); return { getState() { return state;
Two things this mini version makes visible: first, calling the reducer once with { type: "@@INIT" } and no state is how the reducer's default parameter (state = { value: 0 }) becomes the store's actual initial state — a real reducer's default-state pattern isn't just a convenience, it's load-bearing. Second, dispatch is the only line that ever reassigns the closed-over state variable — everything else only reads it.
The single-funnel dispatch pattern here is the mechanical foundation that Actions & Reducers builds on directly — that topic covers how combineReducers splits this one store's state tree into independently-testable slice reducers, each responding to the same dispatched actions. It also sets up the contrast with Zustand's direct-set() model and Selectors & Reselect's later point that a subscribe firing doesn't necessarily mean a component re-renders — those are two different layers.
function reducer(state = { items: [] }, action) { if (action.type === "add") { state.items.push(action.payload); // ❌ mutates the existing array in place return state; } return state;}
Redux's change detection (and React-Redux's re-render decisions, and Redux DevTools' time-travel) all rely on comparing state references between dispatches. Mutating the existing object/array and returning the same reference means nothing downstream can tell a change happened at all.
store.subscribe(() => { // ❌ no argument here — subscribe's listener receives NOTHING about what changed console.log("something changed, but what?");});
The listener takes zero arguments by design — call store.getState() inside it (and typically diff it yourself against the last known value, exactly what useSelector does internally) to find out what actually changed.
3. Creating more than one store for "separate" state#
This defeats the single-source-of-truth model — no unified snapshot, no single DevTools timeline, no way to write a selector that reads across both. The correct pattern is one store, with combineReducers splitting the state tree, not the store itself (see Actions & Reducers).
Treat dispatch as the only legal entry point to state changes — no component or module should reach into the store's internals or attempt to bypass it.
Always return new references from reducers, never mutate state in place, even when using plain (non-Toolkit) Redux — this is what every downstream consumer's change-detection depends on.
Keep the store itself a singleton per app — one store, with slices of its state tree per feature, not multiple independent stores.
subscribe listeners fire on every dispatch, regardless of whether the specific slice a given listener cares about changed — this is why raw store.subscribe is rarely used directly in app code; useSelector (from react-redux) wraps it with its own per-selector comparison so components don't re-render on unrelated slice changes (see Selectors & Reselect).
Because dispatch always calls the entire root reducer (which combineReducers fans out to every slice reducer), keep individual reducer functions cheap — they run on every single dispatched action in the app, not just ones relevant to their own slice.
(reducer);
let renderCount = 0;
store.subscribe(() => { renderCount++; });
store.dispatch({ type: "increment" });
store.dispatch({ type: "unknown/action" });
store.dispatch({ type: "increment" });
console.log(renderCount, store.getState());
},
dispatch(action) {
state = reducer(state, action); // the ONLY place state is ever reassigned