Concepts — mini-redux, and Reading an Entire Production Library
Phase 1, parallel with bi-03/bi-04 · Spec area §33, §44 Level 1.
No browser-internals prerequisite. This is the first framework module, deliberately.
0. Orientation
The 60-second version
Redux's store is about a hundred lines. You could write a working version in twenty minutes and it would pass every test you thought to write.
The module is the other eighty lines — the ones you would not have written, each of which closes a specific bug that only appears under conditions your twenty-minute version never encounters. A guard against dispatching inside a reducer. A snapshot of the listener list. An insistence on immutability that looks like ideology and is actually the enabling condition for O(1) change detection.
This is the first framework module deliberately, and not because Redux is important. It is because Redux is the only production library in this track small enough to hold entirely in your head, which makes it the place to learn the skill the rest of the track depends on:
Derive first, then read the source, then account for every line of difference.
Every difference is one of three things — essential architecture, production hardening, or historical accretion — and being able to tell them apart is the capability this whole track is building toward. Redux is where you practise it on a system small enough that you can be exhaustive.
The three ideas that carry the design
- State is replaced, not mutated → change detection is a reference comparison, and time travel is just keeping old references.
- The reducer is pure → replay is deterministic, which is what makes the devtools possible.
- Subscription is unconditional → every listener runs on every dispatch, and selecting what changed is deliberately somebody else's problem.
The third is the interesting one. It looks like a defect and it is the reason the core stays small (§5.7).
What you should be able to do after this module
| Capability | Evidence you have it |
|---|---|
| Hold a whole system in your head | Explain every line of createStore and why it is there |
| Classify complexity | Point at a guard and say which bug it closes, from having reproduced it |
| Argue state-management choices | Compare against fw-03 on debuggability, not on speed |
| Recognise the API-layering pattern | Explain why both middleware and enhancers exist |
1. Why this is first
Redux is the only production library in this track small enough to read end to end. That makes it the Level-1 rung of the §44 reading ladder, and the capability it builds — "I have held an entire real system in my head" — is a prerequisite for facing 30M lines of Chromium, not a consolation prize afterwards.
It is also the cleanest possible demonstration of the §45 loop, because the gap between the naive implementation and the real one is small enough to enumerate completely. Every line of difference has a reason, and you can find all of them.
The store is 100 lines. The interesting part is the 100 lines you would not have written.
2. Mental Model
dispatch(action)
-> assert not already dispatching
-> state = reducer(state, action)
-> notify a *snapshot* of listeners
Three ideas carry the whole design:
- State is replaced, not mutated. Which makes change detection a reference comparison, and
makes time travel a matter of keeping old references. Compare
bi-08: immutable layout results are cacheable for exactly the same reason. - The reducer is pure. Which makes replay deterministic.
- Subscription is unconditional. Every listener is called on every dispatch; selecting what changed is a separate concern (selectors), and pushing it out of the core is why the core stays small.
That last decision is the interesting one, and it is a genuine trade-off rather than an obvious win — see §5.
3. Build order
createStore(reducer)—getState,dispatch,subscribe.combineReducers.- Middleware +
applyMiddleware. - Enhancers — and articulate why these are a different extension point from middleware.
- Selectors with memoisation.
- Action/state recording.
- Time travel.
- Persistence.
Middleware to write: logger, timing, error handling, async.
Write each stage before reading the corresponding Redux source. The whole value is in the diff between your version and theirs.
4. Failure Lab — the bugs the real source defends against
Each of these is a real defence in Redux. Feel the bug first, then find the guard.
- Dispatch inside a reducer. What breaks, and why is a guard better than "don't do that"?
- Subscribe/unsubscribe during notification. Unsubscribe a listener from inside another listener while the notification loop is running. Watch a listener get skipped. This is why the real implementation snapshots the listener list — reproduce the skip, then fix it.
- Mutating state in a reducer with a memoised selector downstream. The selector's reference check says "unchanged," the UI goes stale. This is the strongest possible argument for immutability, and it is much more convincing after you have seen it.
- Middleware that dispatches synchronously in its own path. Find the re-entrancy.
- Getting state during dispatch. What consistency guarantee is at risk?
5. Trade-offs to argue, not memorise
Notify-everyone vs fine-grained subscription. Redux calls every listener on every dispatch
and leaves selection to userland. This keeps the core tiny and makes the framework integration
layer responsible for performance. Vue and signals (fw-03) make the opposite choice and pay
for it in machinery. Neither is right; they optimise different things, and §47 is where you say
what.
Middleware vs enhancers. Middleware wraps dispatch; enhancers wrap createStore itself.
Middleware is the common case; enhancers are strictly more powerful and much rarer. Ask why both
exist rather than one general mechanism — this is your complexity-notebook entry for the module.
Single store vs many. Single store makes time travel and serialisation trivial and makes code-splitting state awkward.
5.5 Deep dive: the guards, and what each one is defending
Stage 4 of the spec is the module. Here is what each guard actually protects, so you can recognise the shape when you design your own systems.
isDispatching — the reducer must see a stable world
if (isDispatching) throw new Error('Reducers may not dispatch actions.');
A reducer is (state, action) => state. If it dispatches, the state it is computing from is being
replaced underneath it. The result is not "slightly wrong" — it is undefined, because the order
of the nested and outer assignments decides the answer.
Why a guard rather than documentation: the failure is silent, non-deterministic, and appears far
from its cause. When a contract violation produces corruption rather than an error, make it an
error. Compare Blink's DCHECK policy (bi-06) — same reasoning, different language.
Listener snapshotting — the notification list is fixed when notification begins
Redux's documented semantic: subscribing or unsubscribing while listeners are being invoked has no effect on the dispatch currently in progress. A listener unsubscribed mid-notification is still called that time; one subscribed mid-notification is not called until the next.
The naive implementation — iterating the live array with an index and splicing on unsubscribe — skips a listener, because removing element i shifts everything after it while your index keeps advancing.
Two lessons:
- Iterating a collection that callbacks can mutate is a bug class, not an edge case. It
appears in event emitters, observer lists, animation frames, and DOM
NodeLists. - The fix is a snapshot, and the cost is one array copy per dispatch only when the list
changed (the
ensureCanMutateNextListenerspattern: copy lazily on write, not eagerly on read).
Immutability — why it is about change detection, not purity
state.items.push(x); // reference unchanged
return state;
A memoised selector compares prevState === nextState, sees no change, and returns a stale result.
The UI does not update. Nothing threw.
Immutability here is not a functional-programming preference. It is what makes O(1) change detection possible:
| Model | Cost to answer "did this change?" |
|---|---|
| Immutable + reference compare | O(1) |
| Deep equality | O(size) |
| Dirty flags | O(1) but you must maintain them everywhere |
Proxy interception (fw-03) | O(1) at write, plus tracking overhead at read |
You have now met the same trade in bi-08 (immutable layout results are cacheable) and bi-09
(immutable display items and fragments). Immutability is the enabling condition for caching,
in a C++ rendering engine exactly as much as in a JS store.
5.6 Deep dive: middleware vs enhancers, resolved
The complexity-notebook entry for this module. The distinction:
middleware : wraps dispatch — (api) => (next) => (action) => ...
enhancer : wraps createStore — (createStore) => (reducer, preloaded) => store
Middleware can: observe actions, transform them, delay them, swallow them, dispatch others.
It cannot: change getState, add store methods, replace the reducer, or alter subscription.
An enhancer can do all of those, because it constructs the store. applyMiddleware is itself an
enhancer — middleware is a special case of enhancer, packaged so that the common case is easy.
The design question to answer in your notebook: why not expose only enhancers, since they are strictly more powerful?
The honest answer has two halves. Enhancers are hard to write correctly and easy to make incompatible with each other (composition order matters and mistakes are subtle), whereas middleware has a trivially composable signature and a well-understood mental model. So the library provides a constrained interface for the 95 % case and an escape hatch for the rest.
That pattern — narrow API for common use, powerful API for rare use, with the narrow one
implemented in terms of the powerful one — is worth naming, because you will design it yourself.
Compare: useState implemented on useReducer; CSS custom properties vs Houdini; hooks vs render
props.
5.7 Deep dive: what Redux deliberately does not do
The core notifies every subscriber on every dispatch, and does not tell them what changed.
That looks like a defect until you ask who should decide relevance:
- The store cannot know which slice a subscriber cares about without being told.
- Being told means a selector API in the core, plus memoisation, plus a dependency notion — you
have re-invented a reactivity system (
fw-03) inside a state container. - So Redux pushes it out to userland, and the framework binding layer (
react-reduxand friends) becomes responsible for not re-rendering everything.
The consequence: Redux's core stays ~200 lines and the ecosystem is enormous. That ratio is itself the finding. When you see a tiny core with a huge ecosystem, the library made a deliberate choice about where complexity should live — and someone still pays for it, just not in the core.
Compare directly with fw-03: Vue and signals make the opposite choice, putting dependency
tracking in the core and paying for it with proxies, dependency graphs, and cleanup. Neither is
correct; §47 is where you say what each optimises.
5.8 Deep dive: time travel, and why it is nearly free here
Given immutable state and pure reducers:
history = [s0, s1, s2, s3] // just references
jumpTo(1) => notify(s1)
Time travel is keeping references and re-notifying. No inverse operations, no snapshots, no diffing.
Now compare a fine-grained reactive system (fw-03): state is scattered across many independent
signals mutated in place, so "the state at time T" is not a value that exists anywhere. Time travel
requires recording per-signal history and replaying it, and any effect with a side effect must be
suppressed on replay.
This is the single sharpest architectural comparison in the framework strand, and it is not about performance. Explicit-update models make the whole state at a moment a first-class value; fine-grained reactive models make individual changes first-class. Debuggability, replay, serialisation, and undo all follow the first; update efficiency follows the second.
Which you want depends on the product — and being able to state the trade in one sentence is what §47 is checking.
6. Then: the reading ladder, Level 1
Read Redux end to end. Answer all eight §44 gate questions for createStore. Specifically
account for every difference between your implementation and theirs — there should be no line
you cannot explain.
Log the reading in LEARNING-LOG.md §3, with the "what surprised me" column
filled honestly.
7. Verification
- All eight build stages implemented
- All five failure-lab bugs reproduced, then fixed
- Every difference between mini-redux and Redux enumerated and explained
-
Eight gate questions answered for
createStore - Complexity-notebook entry: middleware vs enhancers
- §47 matrix row started: Redux's change-detection model
Core terminology
| Term | Definition |
|---|---|
| Store | The object holding state, plus getState / dispatch / subscribe |
| Action | A plain, serialisable object describing what happened — not what to do |
| Reducer | (state, action) => state; pure, which is what makes replay deterministic |
| Pure function | Same inputs → same output, no observable side effects |
| Dispatch | The single entry point through which all state change flows |
isDispatching guard | Prevents a reducer dispatching; converts silent corruption into an error |
| Listener snapshot | Fixing the notification list at dispatch start; prevents skipped listeners |
ensureCanMutateNextListeners | Copy-on-write of the listener array — pay only when it changed |
| Middleware | Wraps dispatch: (api) => (next) => (action) => … |
| Enhancer | Wraps createStore itself; strictly more powerful, much rarer |
| Selector | A function deriving a view of state; memoised so consumers can reference-compare |
| Memoisation | Caching a result keyed on inputs — sound only if state is immutable |
| Time travel | Replaying an action log against an initial state; requires purity + immutability |
| Single source of truth | One store, so "the state at time T" exists as one serialisable value |
Mental models
Immutability is a change-detection mechanism, not a style. This is the reframe that makes the
whole design legible. If state is replaced rather than mutated, "did this change?" is a === b —
O(1), no bookkeeping, no deep comparison.
| Model | Cost to answer "did this change?" |
|---|---|
| Immutable + reference compare | O(1) |
| Deep equality | O(size) |
| Dirty flags | O(1), but you must maintain them everywhere |
Proxy interception (fw-03) | O(1) at write, plus tracking overhead at every read |
You meet this same trade in bi-08 (immutable layout results are cacheable) and bi-09
(immutable display items). Immutability is the enabling condition for caching, in a C++
rendering engine exactly as much as in a JS store.
When a contract violation produces corruption rather than an error, make it an error. That is
what isDispatching is. The failure it prevents is silent, non-deterministic, and surfaces far
from its cause — the worst possible combination. Compare Blink's DCHECK policy (bi-06): same
reasoning, different language.
Iterating a collection that callbacks can mutate is a bug class, not an edge case. The listener
snapshot exists because splicing during iteration skips an element. You will meet this again in
event emitters, observer lists, animation-frame queues, and DOM NodeLists. Recognise the shape
once and you stop writing it.
Narrow API for the common case, powerful API for the rare one — with the narrow one implemented
in terms of the powerful one. That is middleware and enhancers, and applyMiddleware is
literally an enhancer. The same pattern: useState on useReducer; CSS custom properties vs
Houdini; hooks vs render props. Naming it means you can design it deliberately.
"The state at time T exists as a value" is an architectural property with downstream
consequences. Serialisation, undo, crash-report state dumps, and deterministic replay all follow
from it. No amount of update efficiency substitutes, which is why fw-03's finer-grained model
does not simply obsolete this one.
Common misconceptions
-
"Redux is slow because every listener runs on every dispatch." That is the core's design; selection is the binding layer's job. The trade bought a core small enough to read end to end, which is a real and deliberate benefit.
-
"Immutability is a functional-programming preference." It is what makes O(1) change detection possible. Mutate state and a memoised selector's reference check reports "unchanged" and the UI silently goes stale — no error, no stack trace.
-
"The guards are defensive programming." Each closes a specific reproducible bug. Reproduce them before deciding they are ceremony; the listener-skip bug in particular is invisible until you have seen it.
-
"Actions are commands." They describe what happened, not what to do. That is why the log is replayable and why two reducers can respond to one action without knowing about each other.
-
"Middleware and enhancers are redundant." Middleware wraps
dispatch; enhancers wrapcreateStore. Middleware cannot changegetState, add store methods, or replace the reducer. Both exist because enhancers are powerful and easy to get wrong. -
"Redux is obsolete now that we have signals." They optimise different things. Redux has an explicit, printable action log; a reactive graph does not, and every mature reactive framework ships a devtool specifically to compensate. Compare on incident response, not on benchmarks.
-
"
dispatchinside a reducer is just bad practice." It produces undefined behaviour — the answer depends on the order of nested and outer assignments. That is why it throws.
Interview talking points
- "Redux's store is a hundred lines and the interesting part is the hundred lines you wouldn't have written. I use it as a reading exercise: derive it, then read the source, then account for every difference as essential architecture, production hardening, or accretion."
- "Immutability in Redux isn't an FP preference — it's what makes change detection O(1). Mutate state and a memoised selector's reference check says 'unchanged' and the UI goes stale with no error. That's the same reason Blink keeps layout results immutable: immutability is the enabling condition for caching."
- "The listener snapshot is my favourite small lesson in that codebase. Iterating a list that
callbacks can mutate skips an element — and that's a bug class you meet in event emitters,
observer lists and
NodeLists, not an edge case." - "Middleware and enhancers are a design pattern worth naming: a narrow, trivially composable API
for the 95% case, and a powerful one for the rest, with the narrow one implemented in terms of
the powerful one.
applyMiddlewareis an enhancer." - "When I compare Redux against signals I argue debuggability, not performance. Redux gives you 'the state at time T' as one serialisable value, which is what makes time travel, undo and crash state dumps possible. That's an architectural property no update-efficiency win substitutes for."
8. Principal Engineer Review
-
Redux notifies every subscriber on every dispatch. Defend this as good design, then say what it forces every consuming framework to build.
-
Explain why immutability is load-bearing here in terms of change detection cost, not purity.
-
Middleware and enhancers are two extension points. Design a single mechanism that replaces both. What do you lose?
-
Time travel is nearly free in Redux and expensive in a fine-grained reactive system. Explain why from the data model.
-
A team wants to adopt Redux for a form-heavy app with high-frequency local state. Argue against, mechanically.
-
You are asked to add "only notify subscribers whose selected slice changed" to the core. Specify it. What breaks, and where does the cost move?
-
Redux is ~200 lines of core and enormous ecosystem. What does that ratio tell you about where the real design decisions were made?
-
Compare Redux's explicit-update model with signals' automatic dependency tracking on debuggability, not performance. Which would you want at 3am?
Connections to other modules
fw-02(next) — the other half of the Phase-1 pair. Redux is explicit change notification; mini-react is re-run-and-diff. Holding both precisely is whatfw-03's comparison needs.fw-03— the opposite design. Automatic dependency tracking removes an authoring burden and replaces it with an observability burden; §6.8 there tabulates the trade.fw-09— server state is not client state. Query caches exist because Redux's model assumes you own the data, and server state is shared, stale by default, and invalidated by actors you cannot see.fw-11— the classification method (essential / hardening / accretion) is applied at scale to React and Vue. Redux is where you practise it on a system small enough to be exhaustive.bi-06— Blink'sDCHECKpolicy isisDispatching's reasoning in C++: make contract violations loud when their silent form is corruption.bi-08/bi-09— immutable layout results and display items, cacheable for exactly the reason Redux state is reference-comparable.fe-02— subscription lifecycles are ownership. An unsubscribed listener is this module's version of the leak taxonomy there.