fw-01 — Analysis

Required invariants

  1. A reducer sees a stable world. Dispatching inside a reducer means computing state from a moving target; the result is undefined, not merely wrong.
  2. The listener list is fixed when notification begins. Subscribing or unsubscribing mid-dispatch has no effect on the dispatch in progress.
  3. State is replaced, never mutated. This is what makes change detection O(1).
  4. The reducer is pure, so replay is deterministic — which is what makes time travel free.

Why each guard exists

GuardBug without itDetectability
isDispatchingstate computed from a moving targetsilent, nondeterministic
Listener snapshota subscriber is skipped when another unsubscribes mid-loopsilent, intermittent
Immutability (by convention)memoised selectors see no change; UI goes stalesilent

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

ModelCost of "did this change?"
Immutable + reference compareO(1)
Deep equalityO(size)
Dirty flagsO(1), but maintained everywhere
Proxy interceptionO(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.