JavaScript Signals Explained: Native Reactivity Is Coming to the Language
TC39's Signals proposal could unify how all JS frameworks handle reactivity. Here's what signals are, how they work, and why every frontend dev should care.
What if React useState, Vue’s ref, Angular signals, and Svelte stores all had the same DNA? That’s exactly what TC39’s Signals proposal is building — a native, framework-agnostic reactivity primitive baked right into the JavaScript language. No library, no framework required. Just signals.
What Even Are Signals?
A signal is a reactive value container. When it changes, anything that depends on it automatically updates. You’ve seen this pattern before — Vue’s ref, MobX observables, SolidJS’s createSignal, Angular’s built-in signal(). The TC39 difference? This wouldn’t be a framework feature. It’d be a language feature.
There are two core primitives:
Signal.State— a writable reactive value (your source of truth)Signal.Computed— a derived value that re-evaluates lazily when its dependencies change
import { Signal } from "signal-polyfill";
const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);
console.log(doubled.get()); // 0
count.set(5);
console.log(doubled.get()); // 10Key thing: doubled doesn’t eagerly re-run. It recomputes lazily when you call .get(). That’s a deliberate performance win — no unnecessary recomputation.
The TC39 Proposal — Where Did It Come From?
The proposal was co-authored by Rob Eisenberg (previously on the Angular team at Microsoft) with backing from engineers across Angular, Vue, Solid, Ember, MobX, Preact, Qwik, Svelte, and more. That kind of cross-framework alignment is rare. Like, genuinely rare.
The goal isn’t to replace your framework’s reactivity system. It’s to give every framework a shared low-level primitive to build on top of. Think of how Promise standardized async. Multiple async libraries existed before it (jQuery’s Deferred, Bluebird, Q), but once promises went native, everything converged. Signals are aiming for the same thing with reactivity.
How Is This Different from useState?
React’s useState is scoped to a component and tied to React’s rendering lifecycle. Signals are standalone reactive containers — they don’t need a component tree to exist.
// React: state is tied to component lifecycle
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// Signals: standalone, works anywhere
const count = new Signal.State(0);
count.set(count.get() + 1); // no component neededYou can put signal-based state logic in plain .js files, share it between components (or even across different frameworks on the same page), and use reactive logic without a component at all.
Vue's ref vs TC39 Signals
Vue’s ref and computed are spiritually identical to TC39 Signals — not a coincidence since Vue’s team helped shape the proposal.
// Vue
const count = ref(0);
const doubled = computed(() => count.value * 2);
// TC39 Signals (same concept, native JS)
const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);Angular Already Shipped Signals (And It's Great)
If you’re on Angular 17+, you’re already writing signals. Angular’s implementation is the closest production version to the TC39 proposal:
import { signal, computed } from '@angular/core';
const price = signal(10);
const quantity = signal(3);
const total = computed(() => price() * quantity());
console.log(total()); // 30
price.set(20);
console.log(total()); // 60The main API diff: Angular uses call syntax (price()) while the TC39 spec uses .get(). Same reactive semantics, different getter style.
Can You Use Signals Today?
Yes. The signal-polyfill package tracks the current proposal spec and is ready to experiment with:
npm install signal-polyfill signal-utilsimport { Signal } from "signal-polyfill";
import { effect } from "signal-utils/subtle/microtask-effect";
const name = new Signal.State("World");
effect(() => {
console.log(`Hello, ${name.get()}!`);
});
// Logs: Hello, World!
name.set("Signals");
// Logs: Hello, Signals!Notice that effect is deliberately not part of the core TC39 proposal. Effects are tricky to get right (timing, cleanup, cycle detection), so they’re intentionally left to frameworks. The proposal only standardizes the low-level primitives.
Where Is the Proposal Right Now?
As of early 2026, the Signals proposal is actively working toward Stage 2 at TC39. It’s had extensive cross-framework prototyping and multiple browser engine teams are watching closely.
The TC39 stage roadmap to watch:
Stage 2 — Formal spec text drafted; TC39 intends to include it in the language
Stage 3 — Browser engines start implementing (V8, SpiderMonkey, JavaScriptCore)
Stage 4 — Ships in the ECMAScript spec. It’s now standard JavaScript.
Native browser support is likely still 1–2 years away, but the polyfill means you don’t have to wait. And if you’re an Angular dev, you’re already using signals in production.
TL;DR
Signals are reactive value containers — change one, and everything depending on it auto-updates
TC39’s Signals proposal aims to make this a native JavaScript language feature
Angular 17+ already ships signals in production; Vue, Solid, Preact have their own flavors
You can experiment today with
signal-polyfillon npmThis is probably the most impactful JS language proposal for frontend devs right now