Reactivity Models Compared: React, Vue, Angular, Svelte
If you’ve worked across multiple JavaScript frameworks, you’ve noticed they handle state and UI updates very differently. The mental model behind each approach shapes how you structure components, manage side effects, and reason about performance. Here’s a clear breakdown of how React, Vue, Angular, and Svelte think about reactivity right now.
Key Takeaways
- Reactivity is the mechanism that keeps your UI in sync with application state — frameworks differ in how granular that synchronization is.
- React uses coarse-grained reactivity (re-running component functions and diffing a virtual DOM), while Vue, Angular Signals, and Svelte 5 use fine-grained approaches that track dependencies directly.
- The React Compiler in React 19 narrows the performance gap by automating memoization at build time.
- Angular is transitioning from Zone.js-based change detection to a signal-driven, zoneless model.
- Svelte 5 runes replace the older
$:syntax with explicit, compiler-processed reactive primitives that work both inside and outside.sveltefiles.
What “Reactivity” Actually Means
Reactivity is the mechanism that keeps your UI in sync with your application state. When state changes, the framework decides what to update and how. The key difference between frameworks isn’t whether they support reactivity — they all do — it’s how granular that reactivity is.
Coarse-grained reactivity means the framework re-executes component code to figure out what changed. Fine-grained reactivity means the framework already knows exactly which DOM nodes depend on which state, so it skips the re-execution entirely.
Quick Reactivity Model Comparison
| Framework | Reactivity Type | Core Primitive | Update Scope |
|---|---|---|---|
| React 21 | Coarse-grained | useState / hooks | Component subtree |
| Vue 3 | Fine-grained | ref / reactive (Proxy) | Dependency-tracked |
| Angular 19 | Coarse → Fine | Signals + Zone.js (optional) | Component → Signal node |
| Svelte 5 | Fine-grained | Runes ($state, $derived) | Compiled DOM bindings |
React’s Render Cycle and the React Compiler
React’s reactivity model is built around a simple rule: when state changes, the component function re-runs. React reconstructs a virtual DOM, diffs it against the previous version, and commits only the real changes to the DOM.
This coarse-grained approach is forgiving. You can read and transform state in any way, and React will figure it out. The tradeoff is that unnecessary re-renders are easy to introduce.
With React 19 and the React Compiler, manual memoization with useMemo and useCallback is becoming less necessary. The React Compiler can automatically apply many memoization optimizations at build time, reducing the need for manual useMemo and useCallback in some cases.
Vue’s Proxy-Based Reactivity System
Vue 3’s reactivity system uses JavaScript Proxies to intercept reads and writes. When you access a ref or reactive object inside a component or computed, Vue records that dependency automatically. When the value changes, only the parts of the UI that read it are updated.
Vue 3.5 refined this further, improving memory usage and reducing overhead for deeply reactive objects. The result is a system where fine-grained dependency tracking happens at runtime without any compiler step.
The mental model is explicit: wrap state in ref(), derive values with computed(), and handle side effects with watch or watchEffect. Vue’s reactivity works consistently whether you’re inside a .vue file or a plain .js module.
Discover how at OpenReplay.com.
Angular Signals and the Shift Away from Zone.js
Angular’s traditional change detection relied on Zone.js to monkey-patch async operations and trigger checks across the component tree — a coarse-grained approach with significant overhead.
Angular Signals, introduced in Angular 16 and now the recommended reactive primitive, change this fundamentally. A signal() tracks its own consumers. When it updates, only the components and computed values that read it are marked for re-check. Angular is actively moving toward zoneless change detection, where Zone.js is optional and signals drive updates directly.
import { signal, computed } from '@angular/core'
const count = signal(0)
const doubled = computed(() => count() * 2)
This brings Angular’s reactivity model much closer to Vue’s in terms of granularity, while keeping its strong TypeScript integration and dependency injection system.
Svelte 5 Runes: Compiler-Driven Fine-Grained Reactivity
Svelte has always used a compiler to generate efficient update code. Svelte 5 replaces the older $: reactive declarations with runes — a set of explicit reactive primitives that look like function calls but are processed at compile time.
<script>
let count = $state(0)
let doubled = $derived(count * 2)
$effect(() => {
console.log('count changed:', count)
})
</script>
$state declares reactive state, $derived creates computed values, and $effect handles side effects. The compiler uses these runes to generate precise DOM update instructions, so only the specific nodes that depend on changed state are touched.
Svelte 5 runes also work consistently outside .svelte files in .svelte.js modules, solving the earlier friction of needing stores for shared reactive logic.
The Core Trade-Off: Ergonomics vs. Precision
Coarse-grained systems like React are harder to break — you can read state anywhere and the framework handles the rest. Fine-grained systems like Vue, Angular Signals, and Svelte 5 runes are more precise but require you to follow their rules. Violate those rules (like destructuring a reactive proxy or a signal) and reactivity silently breaks.
The good news: a broken reactive binding is usually obvious and quick to fix. A slow component tree caused by unnecessary re-renders is much harder to diagnose.
Choosing the Right Reactivity Model
Each approach reflects a different set of priorities:
- React — maximum flexibility, large ecosystem, compiler-assisted optimization in React 19
- Vue — runtime fine-grained reactivity with a gentle learning curve
- Angular — enterprise-scale apps moving toward signals and zoneless architecture
- Svelte — smallest output, compiler-enforced fine-grained updates with modern runes syntax
Conclusion
The reactivity model you work with shapes how you think about state. React’s coarse-grained, virtual-DOM-based approach offers flexibility at the cost of potential over-rendering — a gap the React Compiler is closing. Vue and Angular Signals track dependencies at runtime for precise updates, while Svelte 5 runes push that precision into the compiler itself, producing minimal output with no runtime reactivity overhead. Understanding these underlying update mechanics — not just the syntax — makes you a more effective developer regardless of which framework you choose.
FAQs
Not directly. Each framework's reactivity system is tightly coupled to its rendering pipeline. However, you can use framework-agnostic state managers like Zustand, Jotai, or Nanostores across projects. These libraries manage state independently and integrate with whichever framework renders the UI.
Not necessarily. Fine-grained systems avoid unnecessary re-renders by default, but they add overhead for dependency tracking. For small components with simple state, React's coarse-grained diffing can be just as fast. Performance differences become meaningful in large component trees with frequent, localized state changes.
No. Starting with Angular 18, zoneless change detection is available as an experimental option, and Angular 19 promotes it further. New projects can rely entirely on signals for change detection. Zone.js remains supported for backward compatibility, but the Angular team recommends migrating toward a signal-based, zoneless architecture.
Svelte 4 used the dollar-colon label syntax to mark reactive statements, which only worked inside Svelte component files. Svelte 5 runes like $state, $derived, and $effect are explicit primitives processed by the compiler. They work in both .svelte and .svelte.js files, making shared reactive logic simpler and more predictable.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.