DEV Community

Cover image for The Evolution of Reactivity: How UI Updates Learned to Take Care of Themselves
Luciano0322
Luciano0322

Posted on

The Evolution of Reactivity: How UI Updates Learned to Take Care of Themselves

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?

Who initiates updates? Strategy Typical Implementation Keywords
Framework asks the data Pull loops, diffing dirty-checking, VDOM diff
Data notifies the framework Push watchers, signals observable, effect

Most modern frameworks are actually hybrids:
Data pushes invalidation → framework pulls the computations or diffs at the right moment.

We’ll compare the four major models shortly—this hybrid nature becomes obvious when you put them side-by-side.


Four Models of Reactivity

If we place the major approaches on a Pull ↔ Push spectrum, we get a clear timeline of how reactivity evolved.

pull push timeline

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

Dirty checking flow

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

vdom flow

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

observer flow

Dependencies are established through getters.
Only the exact watchers dependent on the changed value re-run.
This significantly reduces unnecessary recalculation.


Fine-Grained Signals

signal flow

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-grained system,
why can’t everything be pure Push?
Why do we still need Pull at all?

That’s what we’ll explore in the next article:
Pull-based vs Push-based—what problems do they really solve?

Top comments (0)