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/Store
beginner20 min read·Updated Jun 2025

Store

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.

Related topics

Actions & ReducersData Structures in JS (Map, Set, WeakMap, WeakRef, typed arrays)ProZustandPro

On this page

20m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

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.

import { createStore } from "redux";
 
function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case "counter/incremented":
      return { value: state.value + 1 };
    default:
      return state;
  }
}
 
const store = createStore(counterReducer);
 
store.subscribe(() => console.log("state changed:", store.getState()));
store.dispatch({ type: "counter/incremented" }); // logs: state changed: { value: 1 }

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.

The store's four-method surface#

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.

Single source of truth, in practice#

const state = store.getState();
// { counter: { value: 1 }, user: { name: null }, cart: { items: [] } }

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.

Contrast with a library-managed external store#

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.


Try It#

Predict the outcome before checking the solution.

import { createStore } from "redux";
 
function reducer(state = { count: 0 }, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    default:
      return state;
  }
}
 
const store = createStore







What do renderCount and store.getState() log?

Solution

3 { count: 2 }.

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


Implement It Yourself#

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.


Under the Hood#

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.


Common Mistakes#

1. Mutating state directly inside the reducer#

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.

2. Expecting subscribe to tell you what changed#

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#

const counterStore = createStore(counterReducer); // ❌
const userStore = createStore(userReducer);        // ❌

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


Best Practices#

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

Performance Tips#

  • 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
listeners.forEach((listener) => listener());
return action;
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
const store = createMiniStore((state = { value: 0 }, action) =>
action.type === "inc" ? { value: state.value + 1 } : state
);
store.subscribe(() => console.log("changed:", store.getState()));
store.dispatch({ type: "inc" }); // logs: changed: { value: 1 }