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