fw-01 — Analysis
Required invariants
- A reducer sees a stable world. Dispatching inside a reducer means computing state from a moving target; the result is undefined, not merely wrong.
- The listener list is fixed when notification begins. Subscribing or unsubscribing mid-dispatch has no effect on the dispatch in progress.
- State is replaced, never mutated. This is what makes change detection O(1).
- The reducer is pure, so replay is deterministic — which is what makes time travel free.
Why each guard exists
| Guard | Bug without it | Detectability |
|---|---|---|
isDispatching | state computed from a moving target | silent, nondeterministic |
| Listener snapshot | a subscriber is skipped when another unsubscribes mid-loop | silent, intermittent |
| Immutability (by convention) | memoised selectors see no change; UI goes stale | silent |
All three failures are silent. That is the argument for guards over documentation: when a contract
violation produces corruption rather than an error, make it an error. The same reasoning produces
Blink's DCHECK policy.
The change-detection cost table
| Model | Cost of "did this change?" |
|---|---|
| Immutable + reference compare | O(1) |
| Deep equality | O(size) |
| Dirty flags | O(1), but maintained everywhere |
| Proxy interception | O(1) at write, plus tracking at read |
Immutability here is not a stylistic preference; it is the enabling condition for cheap change
detection — the same role it plays for layout-result caching (bi-08) and display items (bi-09).
What Redux deliberately does not do
The core notifies every subscriber on every dispatch and does not say what changed. Pushing selection to userland is what keeps the core ~200 lines — and what makes the ecosystem enormous. That ratio is the finding: a tiny core with a huge ecosystem means the library decided where complexity should live, and someone still pays for it.