An observable store holds N state atoms. Each reaction (a UI binding, a computed value, an effect) has a dependency set D built automatically the first time it runs, by recording which atoms it actually reads:
D(reaction_j) = { atom_i : reaction_j read atom_i on its last run }
When atom i mutates, a fine-grained store re-runs only the reactions where i โ D(reaction_j):
runs(mutation) = |{ j : i โ D(reaction_j) }| (fine-grained)
runs(mutation) = |reactions| (naive broadcast)
This is exactly the trade-off between the article's two families of mobile state patterns: MobX-style observables use automatic dependency tracking and re-render only what changed, while a naive global broadcast store (a Context/EventBus with no memoized selectors) notifies every subscriber on every change, however small.
- Mode buttons โ switch the store between fine-grained tracking and naive broadcast; the wiring and mutation stream stay identical, only the propagation rule changes.
- State atoms slider โ grows or shrinks the inner ring of observable state.
- Reactions slider โ grows or shrinks the observer graph and rewires random dependency subsets.
- Dependency density slider โ sets how large a share of the atoms a fresh reaction depends on, from sparse (~15%) to nearly global (~80%) subscriptions.
- Auto-mutate interval โ 0s pauses the automatic stream; drag right to fire mutations continuously.
- Mutate Random Atom โ fires one manual mutation so you can watch a single propagation step.
- Drag the canvas to rotate the ring layout; scroll/pinch to zoom. The history strip along the bottom of the canvas plots re-runs (green) against the broadcast-equivalent (orange) for the last 60 mutations.
- The counters make the asymptotic cost visible: with R reactions each depending on a density-fraction of atoms, broadcast mode does O(R) work per mutation while fine-grained does O(|D|) โ the "re-runs avoided" percentage is exactly the efficiency MobX/Recoil-style fine-grained reactivity buys a mobile app's render loop.