The TC39 proposal to add signals to JavaScript lists its collaborators right in the README. Angular is on it. So are Ember, MobX, Preact, Qwik, RxJS, Solid, Svelte, and Vue. Read the list a second time and the absence jumps out: React, the framework that taught a generation how to think about UI state, is not there.
That gap is not an oversight or a scheduling problem. It is a decision, argued in public by React's own team, and it sits at the center of the sharpest disagreement in frontend today. The rest of the ecosystem spent fifteen years wandering away from an idea Knockout shipped in 2010, then quietly rebuilt it five times over. Signals are that idea. This is how they won almost everywhere, and why the one holdout might still be right.
The 2010 idea everyone walked away from
Knockout.js shipped in July 2010. It gave developers three primitives: observable for a piece of state, computed for a value derived from other state, and later pureComputed for the same thing without side effects. The clever part was invisible. A computed figured out which observables it depended on by watching which ones it read while it ran. Change an input, and only the computeds that actually touched it recalculated.
That is a signal. The name came later.
Then React arrived in 2013 and flipped the model: re-render the component, diff a virtual tree, and let the framework find the delta. Re-running a whole function on every change was easier to reason about, and the virtual DOM made it fast enough to win. The industry followed React's lead for most of a decade. Fine-grained reactivity never died, Vue kept it at its core from 2014 and MobX carried the Knockout torch into React apps from 2015, but it stopped being the default thing people reached for.
Solid pulled the pendulum back. Ryan Carniato rebuilt the Knockout model with modern ergonomics, gave the primitive a crisp name, and spent years explaining in public why fine-grained updates beat re-rendering. By 2022 the word had stuck. Preact shipped @preact/signals, Angular published a proposal to add them, and Qwik built its whole resumability story on top of them. The idea was back, and this time the term traveled with it.
What a signal actually is
Strip away framework syntax and every signals implementation has the same three parts.
A state signal holds a writable value. A computed signal derives a value from other signals and caches it. An effect runs a side effect and re-runs when the signals it read change. Knockout's three primitives, essentially unchanged in fifteen years.
Two properties make the model work, and both are easy to get subtly wrong.
The first is automatic dependency tracking. A computed does not declare what it depends on. It discovers its dependencies by running and recording every signal it reads. There is no dependency array to keep in sync, which is the single biggest ergonomic gap between signals and React's useMemo and useEffect. If a branch of your computation reads a signal only sometimes, the dependency set quietly adjusts on the next run.
Laziness is the second property, and it is what lets the model scale. A computed does not recalculate when its inputs change. It marks itself stale and recomputes only when something reads it. In the API the TC39 polyfill exposes, that looks like this:
import { Signal } from "signal-polyfill";
const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);
doubled.get(); // 0 (the computation runs for the first time here)
count.set(5); // nothing recomputes yet
doubled.get(); // 10 (recomputes now, only because it was read)A value nobody reads costs nothing, no matter how often its inputs churn. That is pull-based evaluation, and it is the property that keeps a large reactive graph affordable.
The hard property is the one users only notice when it breaks. Picture a diamond. State A feeds two computeds, B and C, and a third computed D reads both of them. Change A, and a naive system might recompute D once when B updates and again when C updates. For an instant D holds a value built from the new B and the stale C. That inconsistent intermediate is called a glitch, and chasing glitches out is most of what makes a reactive library hard to write.
A correct signals graph computes D exactly once, after both B and C have settled, using version counters or topological ordering to know when its inputs are genuinely ready. The proposal makes this a guarantee: computations are glitch-free, and in its words "no unnecessary calculations are ever performed." Every framework had to solve this independently. Most of them got it wrong at least once before they got it right.

Why the standard lives in Signal.subtle
The proposed API is small. Signal.State for writable cells, Signal.Computed for derived values, and a Signal.subtle.Watcher that frameworks use to learn when signals change so they can schedule a render. There is also Signal.subtle.untrack, for reading a signal without registering it as a dependency.
That subtle namespace is the tell. It borrows the convention from crypto.subtle: the things inside are sharp, easy to misuse, and not meant for everyday application code. You are not supposed to write new Signal.State(0) in a component. Your framework's signal(), ref(), or $state will wrap it, and you keep writing the syntax you already know.
So if application developers never touch it, who is the standard for?
The answer is interoperability, and it is the most interesting argument in the whole proposal. Every framework ships its own reactive graph today, and that graph is welded to the framework's own view engine. A form-validation library written against Angular signals cannot drive a Svelte component. A query cache built on Vue's reactivity cannot feed a Solid app. Standardize the primitive and a reactive data layer could be written once and consumed under any of them. That is why MobX, RxJS, and Starbeam authors sit on the collaborator list beside the framework teams. They want the substrate, not the syntax.
Promises are the precedent everyone cites. Libraries shipped competing thenable implementations for years, the Promises/A+ spec pinned down the semantics, and then the language absorbed the pattern. Signals are walking the same path, slowly. The proposal reached Stage 1 in 2024 and has stayed there. Its collaborators say they want to be "especially conservative" about advancing it, so they do not standardize something the ecosystem comes to regret.
That caution cuts both ways. The frameworks already shipped their own signals and are still actively revising them. Standardizing now risks freezing a design that its own inventors keep changing underneath the spec.
The frameworks already agreed
The proposal is cautious. The frameworks were not. By 2026 the major ones run on signals, and the grid below is less a forecast than a status report.
| Framework | Reactive primitives | Signals status | DOM update model |
|---|---|---|---|
| Solid | createSignal / createMemo / createEffect | Core since 1.0 (2021) | Direct DOM, no VDOM |
| Svelte 5 | $state / $derived / $effect | Stable Oct 2024, signals under the hood | Compiled, no VDOM |
| Angular | signal / computed / effect / linkedSignal | Dev preview v16 (2023), stable v20 (2025) | Direct DOM, was Zone.js dirty-checking |
| Vue | ref / computed / watchEffect | Reactive core since 2014 | VDOM, Vapor mode removes it |
| Preact | signal / computed / effect | @preact/signals since 2022 | VDOM, signals bypass diffing |
| Qwik | useSignal / useComputed$ | Core, paired with resumability | Resumable, no hydration |
The headline migration is Angular. Signals landed as a developer preview in version 16 during 2023 and graduated to stable in version 20 in 2025. The deeper change came next. Version 21, released in November 2025, stopped bundling Zone.js by default. Zone.js had been Angular's change-detection engine for a decade, a library that monkey-patched every async browser API to know when to re-render. Angular replaced that entire mechanism with signals. Ripping out change detection and rebuilding it on a new primitive is not a feature for a framework that size. It is a foundation swap.
Svelte did something quieter and stranger. Svelte 4 had its own famous reactivity: assignments triggered updates, and the $: label marked reactive statements. Svelte 5 kept the developer-facing simplicity, introduced runes ($state, $derived, $effect), and rebuilt the entire engine on signals underneath. The syntax still reads like plain variables. The runtime is now a signal graph. Most existing components kept working through the change, which was the part that took the real engineering.
Vue never abandoned fine-grained reactivity, so its story is about going faster. Vue 3.5 moved to a pull-based algorithm closer to Preact's. Vue 3.6 went further and ported alien-signals, a research implementation by Johnson Chu built around a doubly linked list for dependencies and version-based dirty checking. The reported result was roughly 13 percent less memory when allocating large numbers of reactive instances, plus strong numbers on the js-reactivity-benchmark suite. Vapor Mode, landing in the same release line, lets Vue render without a virtual DOM at all, which only makes sense because the reactivity is already precise enough to update the DOM directly.
The pattern across all of them is the same. Signals replaced whatever coarse update mechanism came before: virtual DOM diffing in Vue and Preact, Zone.js dirty-checking in Angular, the old compiler tracking in Svelte. Once reactivity is precise enough to know which DOM node depends on which value, the coarse pass becomes redundant overhead. Solid never had a virtual DOM. Svelte compiles past it. Vue is building an exit door. It is the same do-the-least-work instinct behind islands architecture and the resumability that Qwik is built on: update as close to the change as possible, and skip everything else.
React's no is not stubbornness
React is the one major framework that runs on none of this, and the team has explained the refusal more than once.

Dan Abramov's version of the argument is about a specific class of bug. In a signals model you wire up reactions once and then mutate state, trusting the graph to push updates to the right places. The risk is divergence: the code that builds the UI the first time and the code that updates it later run different paths, so it is possible to set up the initial render correctly while forgetting to make some value reactive, and a later change then fails silently. Abramov's framing is that "the user doesn't care whether it's initialization or update. The end result is supposed to be the same."
React enforces that sameness by brute force. It re-runs the component function top to bottom on every render, so initialization and update are literally the same code. You cannot forget to make something reactive, because everything is reactive by default. The cost is real and obvious: React does more work than changed, re-running functions and diffing trees to find the small set of things that actually moved.
For years React asked developers to claw that cost back by hand, with useMemo, useCallback, and React.memo. That manual memoization is the tax everyone complains about, and it is precisely the work a signals graph does for free. React's answer is not to adopt signals. It is to make a compiler do the memoization instead. The React Compiler analyzes components and inserts the caching you would have written by hand, which lets the team keep the re-run-everything model while erasing most of its overhead.
RedMonk framed the split cleanly. Signals-based frameworks change the model to make it efficient. React keeps the model and adds a compiler to optimize it. Two routes to the same destination, attacking different layers of the stack. Neither one is obviously wrong, and that is exactly why the disagreement has lasted.
What complicates React's position is its own ecosystem. Plenty of React developers already want signal-like ergonomics, and they reach for Jotai, Valtio, Zustand, or @preact/signals-react to get them. React 18 even shipped useSyncExternalStore specifically so external stores could integrate without tearing. The appetite for fine-grained state clearly exists inside React. The team's stance is narrower than 'signals are bad.' It is that signals should not be React's core rendering primitive.
What changes for the rest of us
Pick a framework today and signals are no longer a differentiator. Angular, Vue, Svelte, and Solid all teach the same three concepts under different spellings. Learn auto-tracked derived state once and it transfers, which simply was not true five years ago. That portability of the mental model is underrated, and it is new.
For library authors the stakes are higher, and the TC39 standard is the actual prize. A framework-agnostic reactive primitive means a validation engine, a data cache, or an animation system could be written against one signals API and run under anything that adopts it. That is the world the MobX and RxJS maintainers are working toward. It does not exist yet, and it depends entirely on the standard landing and on frameworks building on the native primitive instead of keeping their own forks.

On performance, resist the reflex that signals are simply faster. Fine-grained updates cut wasted re-renders, which helps with main-thread work and metrics like Interaction to Next Paint. But a signal graph carries its own costs: every dependency edge is memory, every effect is a live subscription, and a badly shaped graph can do more bookkeeping than a coarse re-render would. Vue did not port alien-signals for sport. It did it because naive signal implementations got expensive at scale. Signals move the cost around. They do not delete it.
The migrations are not free either. Angular teams going zoneless inherit a real testing and library-compatibility tail. Svelte 4 codebases adopting runes hit edge cases the migration tool cannot auto-fix. The convergence is genuine, but 'everything runs on signals now' hides a lot of incremental, unglamorous work that lands on application teams rather than framework authors.
The question the standard cannot answer
A language primitive only matters if the dominant players use it. Promises became universal because everyone, including the framework that mattered most at the time, eventually adopted them. Signals face a different shape of problem. The framework with the largest mindshare has looked at the primitive, understood it completely, and chosen a different architecture on purpose.
If React never wraps Signal.State, the native signal becomes the thing every framework except the biggest one uses directly. It stays valuable for the interop libraries that want to span frameworks. It stays mostly invisible to the millions of developers who write React every day and let the compiler handle what signals handle elsewhere.
There is an irony in the timing. The industry spent fifteen years routing around an idea Knockout shipped in 2010, rebuilt it five times under five names, and is now trying to carve it into the language, just as the frameworks that inspired the standard keep rewriting their own implementations. Vue is on its third reactivity engine. Angular just swapped its core. The thing being standardized is still moving.
So the open question is not whether signals won. They did, nearly everywhere. It is whether a standard built from a moving target, and declined by the single most popular framework, becomes the shared foundation its authors imagine, or a well-engineered primitive that most application developers never call by name.
