The Evolution of Reactivity: How UI Updates Learned to Take Care of Themselves
Source: Dev.to
A Brief History
Back in 2010, Knockout.js introduced the ideas of Observable and Computed to the frontend world.
For the first time, the browser had a practical way to let data speak first—and have the UI follow automatically.
From that moment on, the core debate behind every major framework became:
Should the framework actively “check” the data, or should the data proactively “notify” the framework?
Looking back, Knockout didn’t win the popularity race, but it did plant the conceptual seeds that eventually shaped the big three: Angular, React, and Vue. The idea of automatic reactivity changed everything.
This article focuses on UI reactivity (related to FRP, but not identical), and how its evolution influenced Angular, React, Vue, and eventually the modern Signals movement.
What Reactivity Really Means
Reactivity transforms UI updates from:
❌ “Manually change the DOM when data changes”
into
✅ “Describe what the UI should look like—the system handles the rest.”
Core Principles
Declarative
You specify what the UI should look like. The system decides how updates reach the screen.
Dependency tracking
When the program first reads a value, the system silently records “who depends on what.”
Change propagation
When data changes, the system sends invalidation signals to all dependents and updates only what’s necessary.
Why it matters?
- Reduces mental load: No more wondering “Did I remember to update X?”
- Performance: Update only the parts that actually changed—no more full-page redraws.
- Clearer data flow: Easier debugging, predictable behavior.
Two Core Strategies: Who Speaks First?
| Strategy | Typical Implementation | Keywords |
|---|---|---|
| Pull (Framework asks the data) | loops, diffing | dirty‑checking, VDOM diff |
| Push (Data notifies the framework) | watchers, signals | observable, effect |
Most modern frameworks are actually hybrids: data pushes invalidation → framework pulls the computations or diffs at the right moment.
Four Models of Reactivity
If we place the major approaches on a Pull ↔ Push spectrum, we get a clear timeline of how reactivity evolved.

Summary Table
| Model | How Updates Flow | Push/Pull Position | Granularity | Representative Frameworks |
|---|---|---|---|---|
| Dirty‑checking | $digest scans all $watch → sync updates | Pure Pull | Per‑expression; performance degrades with watchers | AngularJS 1.x |
| Virtual DOM diff | setState pushes a dirty flag → batch re‑render → VDOM diff → DOM patch | Hybrid | Component subtree; simple mental model, but can over‑render | React, Preact, Vue 2 |
| Watcher / Observable Graph | setter pushes → watchers recompute only their subtree | Push‑leaning | Getter‑based dependency tracking; finer granularity | Vue 2 Watchers, MobX |
| Fine‑grained Signals | setter pushes → values lazily pull recomputation → direct DOM updates | Hybrid (runtime) or near‑pure push (compile‑time) | Prop‑level or DOM‑node‑level precision; no VDOM | Solid.js, Angular Signals, Svelte 5 Runes |
Model Details
Dirty‑Checking

A pure pull model. The framework repeatedly scans every watched expression to check what changed. Predictable but expensive—performance scales with how many watchers you have.
Virtual DOM Diff

React adds batching, invalidation flags, and a diff phase. The push step marks components as dirty, and the pull step resolves their new UI through diffing. Great DX, but sometimes performs unnecessary work.
Watcher / Observable Graph

Dependencies are established through getters. Only the exact watchers dependent on the changed value re‑run, significantly reducing unnecessary recalculation.
Fine‑Grained Signals

Instead of working with large component trees, Signals operate on the smallest possible reactive units:
- values
- memos
- even raw DOM nodes
Runtime signals (Solid, Angular Signals) use push invalidation + pull (lazy) recomputation.
Compile‑time signals (Svelte 5) push most of the work into the compiler, approaching a pure push model.
Key Comparisons
1. Push vs Pull — Who initiates the work?
- Dirty‑checking: pure pull
- Virtual DOM: push (dirty) → pull (diff)
- Watcher / Proxy / Signals: push invalidation → pull evaluation (lazy)
- Compile‑time Signals: dependencies fixed at compile‑time, close to pure push
2. Dependency Precision — How exact is the system?
- Virtual DOM: knows “which component subtree might need updates.”
- Runtime Signals / Proxies: know “exactly which memo or DOM node changed.”
- Compile‑time Signals: emit final DOM operations directly—highest precision.
3. Scheduling — When do updates actually run?
- React / Solid: batch via microtasks; collapse multiple writes into one tick
- Vue 3: job queue
- Dirty‑checking: synchronous loop; cost grows linearly
4. Mental Model — What does it feel like to code?
- Signals / MobX: feels like “just changing values”—the system handles propagation
- Virtual DOM: embrace the “render ≠ paint” declarative workflow
- Compile‑time Signals: closer to plain JavaScript; compiler + IDE maintain predictability
Conclusion
The history of reactivity is essentially the history of balancing Push and Pull.
- We started with the pure Pull world of dirty‑checking.
- Then hybrid models like the Virtual DOM emerged.
- Later, dependency‑graph‑based systems created fine‑grained Push + Pull hybrids.
Runtime signals tend to use push invalidation + pull (lazy) evaluation, while compile‑time signals shift the work to build time, bringing us close to pure Push reactivity.
By now, you should have a clear picture of how each model differs in:
- Who initiates updates?
- How far the effects propagate?
But there’s one crucial question left unanswered:
Even in a fine‑gr