Signals Won the Framework War Except in React: A Developer Guide to Runes and Fine-Grained Reactivity in 2026
Most reactivity problems stem from treating state updates as coarse-grained events. This guide shows how signals and Svelte runes deliver fine-grained reactivity that React deliberately avoided.
Signals Won the Framework War Except in React: A Developer Guide to Runes and Fine-Grained Reactivity in 2026
Most reactivity problems stem from treating state updates as coarse-grained events that trigger wholesale component re-renders. By 2026, nearly every major framework except React adopted signals—a pattern that tracks dependencies at the expression level and updates only the precise DOM nodes affected by state changes.
The framework landscape divided cleanly. Solid, Vue 3, Angular 17+, Svelte 5, and Preact Signals all converged on fine-grained reactivity primitives. React chose virtual DOM reconciliation with compiler optimization. This isn't a theoretical debate. The choice determines how developers reason about performance, how frameworks handle updates, and what mental models teams must internalize.
Understanding Signals: The Mental Model That Changed Frameworks
Signals represent a fundamental shift in how frameworks track dependencies. Traditional reactive systems re-run entire component functions when state changes. Signals track which expressions read which state values and update only those expressions.
The core primitive is deceptively simple: a signal wraps a value and notifies subscribers when that value changes. The framework analyzes your code at compile time or runtime to build a dependency graph. When a signal updates, only the computations and DOM nodes that depend on that signal re-execute.
flowchart TD
Signal["count signal (value: 5)"]
Derived["doubled computed (count × 2)"]
Effect1["DOM text node: 'Count: 5'"]
Effect2["DOM text node: 'Doubled: 10'"]
Signal --> Derived
Signal --> Effect1
Derived --> Effect2
classDef dataStore fill:#1e293b,stroke:#64ffda,color:#e2e8f0
classDef uiComponent fill:#3b0764,stroke:#a855f7,color:#e9d5ff
class Signal,Derived dataStore
class Effect1,Effect2 uiComponent
The implication here is surgical precision. When count updates from 5 to 6, the framework knows exactly which computations depend on count and which DOM text nodes need updates. No virtual DOM diffing. No component function re-execution. Just targeted updates to the affected nodes.
This distinction is critical. React's virtual DOM approach optimizes reconciliation speed. Signals eliminate reconciliation entirely for most updates.
Svelte 5 Runes: The New Signal-Based Paradigm
Svelte 5 introduced runes—a signal-based reactivity system that replaced the implicit reactivity of Svelte 3 and 4. The syntax mirrors signals from other frameworks but compiles to highly optimized imperative code.
<script lang="ts">
// State signal
let count = $state(0);
// Derived signal (updates automatically when count changes)
let doubled = $derived(count * 2);
// Effect (runs when dependencies change)
$effect(() => {
console.log(`Count changed to ${count}`);
});
function increment() {
count += 1; // Signal update
}
</script>
<button onclick={increment}>
Count: {count}
</button>
<p>Doubled: {doubled}</p>The $state rune creates a signal. The $derived rune creates a computed value that automatically recalculates when dependencies change. The $effect rune runs side effects when dependencies update.
Svelte's compiler analyzes these runes and generates code that tracks dependencies precisely. When count updates, only the {count} expression in the button text and the doubled computation re-execute. The paragraph containing {doubled} updates only if doubled actually changed value.
This matters because developers write declarative code while the framework generates performant imperative updates. The mental model stays simple. The execution stays fast.

Fine-Grained Reactivity vs React's Virtual DOM: A Technical Comparison
The architectural divide between signals and virtual DOM represents fundamentally different tradeoffs. React optimizes for predictability and component isolation. Signal-based frameworks optimize for update precision and minimal overhead.
flowchart LR
subgraph SignalApproach["Signals: Direct DOM Updates"]
S1["State signal changes"]
S2["Dependency graph lookup"]
S3["Update affected DOM nodes"]
S1 --> S2 --> S3
end
subgraph VDOMApproach["Virtual DOM: Reconciliation"]
V1["State changes"]
V2["Component re-renders"]
V3["Virtual DOM diff"]
V4["Apply DOM patches"]
V1 --> V2 --> V3 --> V4
end
classDef framework fill:#064e3b,stroke:#34d399,color:#6ee7b7
class S2,S3,V3,V4 framework
React's approach runs component functions top-down when state changes. The framework builds a new virtual DOM tree, diffs it against the previous tree, and applies minimal DOM updates. This adds overhead but provides strong guarantees: component functions are pure, updates are batched, and reconciliation is predictable.
Signal-based frameworks skip reconciliation. When a signal updates, the framework directly updates the DOM nodes that depend on that signal. No component re-renders. No virtual DOM. Just targeted mutations.
The failure mode here is subtle but expensive. In React, an update to a parent component can trigger cascading re-renders of child components even when those children don't consume the changed state. Developers mitigate this with React.memo, useMemo, and useCallback. In signal-based frameworks, this problem doesn't exist—only the expressions that read the changed signal re-execute.
Signals in Solid, Angular, Vue, and Preact: Cross-Framework Examples
The convergence on signals across frameworks proves the pattern's universality. Each framework adapted signals to its existing architecture while preserving the core mental model.
Solid pioneered signals in modern frameworks with a syntax that influenced later implementations:
import { createSignal, createEffect } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const doubled = () => count() * 2;
createEffect(() => {
console.log(`Count: ${count()}`);
});
return (
<button onClick={() => setCount(count() + 1)}>
Count: {count()} | Doubled: {doubled()}
</button>
);
}Vue 3 introduced ref and computed as signal primitives that integrate with the Options API and Composition API:
import { ref, computed, watchEffect } from "vue";
export default {
setup() {
const count = ref(0);
const doubled = computed(() => count.value * 2);
watchEffect(() => {
console.log(`Count: ${count.value}`);
});
return { count, doubled };
},
};Angular 17+ adopted signals with zone-less change detection:
import { Component, signal, computed, effect } from "@angular/core";
@Component({
selector: "app-counter",
template: `
<button (click)="increment()">
Count: {{ count() }} | Doubled: {{ doubled() }}
</button>
`,
})
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
constructor() {
effect(() => {
console.log(`Count: ${this.count()}`);
});
}
increment() {
this.count.update((n) => n + 1);
}
}Preact Signals provide a minimal signal implementation that works with Preact or React:
import { signal, computed, effect } from "@preact/signals-react";
const count = signal(0);
const doubled = computed(() => count.value * 2);
effect(() => {
console.log(`Count: ${count.value}`);
});
function Counter() {
return (
<button onClick={() => count.value++}>
Count: {count} | Doubled: {doubled}
</button>
);
}The syntax varies but the pattern remains consistent: state signals, derived computations, and side effects that automatically track dependencies.

Why React Chose a Different Path (and What That Means for You)
React's continued commitment to virtual DOM reconciliation instead of signals stems from architectural decisions made years before signals gained traction. The framework optimized for component isolation, predictable updates, and a programming model where component functions remain pure.
Signals break component boundaries. A signal defined in one component can be read by any other component that imports it. This enables powerful patterns but violates React's principle that data flows down through props and up through callbacks.
flowchart TD
ReactModel["React: Component Trees"]
SignalModel["Signals: Direct Subscriptions"]
ReactModel --> Props["Props flow down"]
ReactModel --> Callbacks["Events flow up"]
ReactModel --> Isolation["Components isolated by default"]
SignalModel --> Global["Signals accessible anywhere"]
SignalModel --> Direct["Direct dependency tracking"]
SignalModel --> Shared["Shared state without prop drilling"]
classDef framework fill:#064e3b,stroke:#34d399,color:#6ee7b7
classDef dataStore fill:#1e293b,stroke:#64ffda,color:#e2e8f0
class Props,Callbacks,Isolation framework
class Global,Direct,Shared dataStore
React 19 doubled down on compiler optimization instead. The React Compiler automatically memoizes component outputs and detects when re-renders can be skipped. This delivers signal-like performance while preserving React's component model.
The implication here is that React remains viable for teams that value component isolation and explicit data flow over minimal update overhead. The performance gap narrows with compiler optimization. The mental model gap remains.
For new projects in 2026, teams choosing React accept that they're working against the industry trend toward fine-grained reactivity. That's not inherently wrong. React's ecosystem, tooling, and hiring pool remain unmatched. But developers should understand they're choosing explicit tradeoffs rather than following the reactivity revolution.
Migrating Mental Models: From useEffect to $derived and $effect
The transition from React hooks to signals requires rethinking how effects and computations work. React's useEffect runs after render and requires explicit dependency arrays. Signals track dependencies automatically.
flowchart TD
StateChange["State updates"]
ReactPath["React: useEffect dependency check"]
SignalPath["Signals: Automatic dependency tracking"]
StateChange --> ReactPath
StateChange --> SignalPath
ReactPath --> Manual["Developer lists dependencies"]
ReactPath --> Stale["Risk: Stale closures if wrong"]
ReactPath --> Cleanup["Manual cleanup functions"]
SignalPath --> Auto["Framework tracks reads"]
SignalPath --> Precise["Always current values"]
SignalPath --> AutoClean["Automatic cleanup"]
style Stale stroke:#ef4444,fill:#450a0a,color:#fca5a5
classDef framework fill:#064e3b,stroke:#34d399,color:#6ee7b7
classDef userAction fill:#1e3a8a,stroke:#60a5fa,color:#e0eaff
class StateChange userAction
class ReactPath,SignalPath,Auto,Precise framework
Consider a React component that fetches data when a filter changes:
// React approach
function UserList({ filter }) {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch(`/api/users?filter=${filter}`)
.then((r) => r.json())
.then(setUsers);
}, [filter]); // Must remember to include filter
return <div>{users.map((u) => u.name).join(", ")}</div>;
}The equivalent Svelte 5 rune approach:
<script lang="ts">
let filter = $state("");
let users = $state([]);
$effect(() => {
// Automatically re-runs when filter changes
fetch(`/api/users?filter=${filter}`)
.then((r) => r.json())
.then((data) => (users = data));
});
</script>
<div>{users.map((u) => u.name).join(", ")}</div>The $effect rune automatically tracks that it reads filter. When filter updates, the effect re-runs. No dependency array. No stale closure bugs. The framework handles it.
For derived values, $derived replaces useMemo:
// React
const filteredUsers = useMemo(
() => users.filter((u) => u.name.includes(search)),
[users, search]
);
// Svelte 5
let filteredUsers = $derived(
users.filter((u) => u.name.includes(search))
);The mental shift is from explicit to automatic. React developers must remember to list dependencies. Signal developers write the computation and trust the framework to track what it reads.
This matters because the entire class of bugs related to stale closures and missing dependencies disappears. The code becomes more maintainable. The developer experience improves.
The 2026 Landscape: Choosing Your Reactivity Model
The framework war ended not with a winner but with a clear division. Fine-grained reactivity through signals dominates new frameworks and major updates. React maintains its virtual DOM approach with compiler optimization. Both models work. Both have tradeoffs.
Choose signals when you need minimal update overhead, automatic dependency tracking, and direct DOM manipulation. Choose React when you value component isolation, a massive ecosystem, and explicit data flow. The performance difference matters less than it did in 2023. The mental model difference remains significant.
For teams migrating from React, the hardest part isn't learning signal syntax. It's unlearning the habits of dependency arrays, memoization hooks, and component re-render optimization. Signals make many React best practices obsolete. That's the point.
That covers the essential patterns for fine-grained reactivity in 2026. Apply these in production and the difference will be immediate.