TC39 Signals Stage 1 Analysis: How React, Vue, and SolidJS Code Will Change
After five years of working on frontends, it feels like roughly half of total development time has been spent wrestling with state management libraries. Burned out on Redux boilerplate, you move to MobX; then Recoil comes out and you switch again; nowadays many teams have settled on Zustand. But if you trace the source of this fatigue, you inevitably arrive at one fact: "JavaScript itself has no reactive state primitive."
The TC39 Signals proposal is an attempt to fill exactly that gap. TC39 is the committee that governs the JavaScript language standard, and Stage 1 means "official discussion has begun"—not a confirmed adoption, but a declaration that the idea will be refined together going forward. Since passing Stage 1 in April 2024, major framework teams have been actively participating in committee work, and looking closely at how close or far each framework is from this standard gives a fairly clear picture of where things are headed. This article examines how the TC39 Signals spec differs from each framework's current implementation, and what our codebases will actually look like once the standard is settled.
Core Concepts
What Is a Signal, and Why Standardize Now?
A Signal is a reactive cell that holds a value. When the value changes, computations that depend on that value are automatically recalculated. MobX's observable, Vue's ref, and SolidJS's createSignal are all variations of this idea. The fact that each framework independently implemented the same concept over 5–10 years is itself a signal that this is a primitive JavaScript needs at the language level.
The public API of TC39 Signals consists of two main pieces:
| API | Role | Framework Equivalent |
|---|---|---|
Signal.State(value) |
A readable and writable reactive cell | ref(), createSignal(), signal() |
Signal.Computed(callback) |
A read-only computation derived from other Signals | computed(), createMemo() |
There is also a separate Signal.subtle namespace, which is a low-level API intended for framework authors. Think of it as "the area where internal implementors control rendering scheduling directly." Average application developers will rarely need to touch it.
One notable point is that effect() is not included in the standard. The omission of the most commonly used side-effect API is an intentional design decision. Because each framework has its own strategy for when and how to batch rendering, having the language standard dictate this would instead become a constraint. Instead, Signal.subtle.Watcher provides the foundation for frameworks to implement this themselves.
import { Signal } from "signal-polyfill";
const counter = new Signal.State(0);
const isEven = new Signal.Computed(() => (counter.get() & 1) === 0);
const parity = new Signal.Computed(() => (isEven.get() ? "even" : "odd"));
// effect is not in the standard, so implement it directly with Watcher
const w = new Signal.subtle.Watcher(() => {
queueMicrotask(() => {
for (const s of w.getPending()) s.get();
w.watch();
});
});
// GC에 수거되지 않도록 반드시 변수에 할당해 참조를 유지해야 한다
const parityLogger = new Signal.Computed(() => console.log(parity.get()));
w.watch(parityLogger);
counter.set(1); // → "odd"
counter.set(2); // → "even"I initially searched hard for effect() hidden somewhere inside Signal.subtle, but it really isn't there. It's an intentional design.
Where Does Each Framework Stand Right Now?
Honestly, when I first saw this table I thought "Angular actually influenced the TC39 proposal directly?" and double-checked. Yes. The Angular Signals (v17+) team participated in the committee, and on top of that, Ryan Carniato—the creator of SolidJS—actively participated in the TC39 committee and made substantive contributions to the API design. So when people say SolidJS is "closest to the original form," it's not an exaggeration—the designer was sitting at the table.
| Framework | Own Implementation | Relationship with TC39 |
|---|---|---|
| SolidJS | createSignal / createMemo |
Closest to the original. Creator Ryan Carniato participates in TC39 committee; experiments to swap internals with a polyfill are underway |
| Angular v17+ | signal() / computed() |
Directly involved in the TC39 proposal, influencing API design |
| Vue 3 | ref() / computed() |
Proxy-based implementation; participating in committee and discussing interoperability |
| Svelte 5 | $state / $derived (Runes) |
Compile-time transformation first; prioritizes compiler optimization over runtime API |
| Preact | signal() / computed() |
Expressed intent to adopt TC39 standard early; API is close |
| React | — | Sidestepping via React Compiler; no plans for direct TC39 integration |
React's departure is striking. There's a reason the phrase "Signals won, React is living off its ecosystem" appears frequently in 2025–2026 community discourse. React has adhered to its philosophy of unidirectional data flow and immutable state, choosing instead to pursue performance through React Compiler (formerly React Forget), which automatically inserts memoization code at build time. This is fundamentally different in philosophy from the mutable reactive cell model of Signals. Personally, I don't think React is wrong, but it's a fact that the center of gravity in the ecosystem has clearly shifted toward Signals.
Practical Application
Example 1: Same Feature, Syntax Compared Across Frameworks
A simple example tracking a counter and its doubled value—placing them side by side makes it viscerally clear how naturally TC39 becomes a common denominator.
// 1. SolidJS (current)
import { createSignal, createMemo } from "solid-js";
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);
// 2. Vue 3 (current)
import { ref, computed } from "vue";
const count = ref(0);
const doubled = computed(() => count.value * 2);
// 3. Angular v17+ (current)
import { signal, computed } from "@angular/core";
const count = signal(0);
const doubled = computed(() => count() * 2);
// 4. TC39 Signals standard (future)
const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);| Item | SolidJS | Vue 3 | Angular | TC39 Standard |
|---|---|---|---|---|
| Reading state | count() |
count.value |
count() |
count.get() |
| Writing state | setCount(n) |
count.value = n |
count.set(n) |
count.set(n) |
| Derived value | createMemo |
computed |
computed |
Signal.Computed |
| Invocation style | Tuple return | Proxy object | Single function | Class instance |
Looking at the API structure, Angular and SolidJS are closest to the TC39 standard. If you're already using either of those two frameworks, the migration path to TC39 will be relatively gentle. From a practical standpoint, this is a fairly meaningful difference—it's natural that Angular and the standard are similar given that the Angular team was sitting at the design table, and if that standard is finalized, Angular codebases will likely face much lower migration costs than other frameworks. Vue's .value proxy approach feels slightly different, and Svelte is at a slightly different layer of comparison entirely, since its compiler transforms $state into runtime Signals.
Example 2: Framework-Independent Business Logic
This is where the biggest change comes when the standard matures. A situation that comes up often in practice: what do you do today when you need the same shopping cart logic in both a React app and a Vue app simultaneously? You either use an external state manager like Zustand as a common layer, or you write the logic twice.
With TC39 Signals as the foundation, it would look like this:
// cart.signals.js — business logic with no dependency on any framework
import { Signal } from "signal-polyfill"; // use polyfill until standard is adopted
export function createCart() {
const items = new Signal.State([]);
const total = new Signal.Computed(() =>
items.get().reduce((sum, item) => sum + item.price, 0)
);
const itemCount = new Signal.Computed(() => items.get().length);
const addItem = (item) => items.set([...items.get(), item]);
const removeItem = (id) => items.set(items.get().filter((item) => item.id !== id));
return { items, total, itemCount, addItem, removeItem };
}The key point is that this file imports no framework whatsoever. No React, no Vue, no Angular. The business logic is separated into a completely portable form.
// React adapter
// useSyncExternalStore는 React 18+에서 외부 스토어를 React 렌더 사이클에 연결하는 훅이다.
// Signal이 바뀌면 onStoreChange를 트리거해 리렌더를 유발하는 구조—
// React의 불변 상태 모델과 Signals의 가변 모델을 연결하는 공식 브릿지라고 보면 된다.
import { useSyncExternalStore } from "react";
import { createCart } from "./cart.signals.js";
const cart = createCart();
function useSignal(signal) {
return useSyncExternalStore(
(onStoreChange) => {
const watcher = new Signal.subtle.Watcher(onStoreChange);
watcher.watch(signal);
return () => watcher.unwatch(signal);
},
() => signal.get()
);
}
function CartTotal() {
const total = useSignal(cart.total);
return <span>Total: {total} USD</span>;
}// Vue 3 adapter — much more concise
import { customRef } from "vue";
import { createCart } from "./cart.signals.js";
const cart = createCart();
function signalRef(signal) {
return customRef((track, trigger) => {
const watcher = new Signal.subtle.Watcher(trigger);
watcher.watch(signal);
return {
get() {
track();
return signal.get();
},
set(value) {
signal.set(value);
trigger();
},
};
});
}An adapter layer is still needed, but the business logic only needs to be written once. The adapters are thin too—on the React side it's a useSyncExternalStore subscription, on the Vue side it's a customRef wrapper.
Pros and Cons
Advantages
| Item | Description |
|---|---|
| Reduced bundle size | Once built into browsers, per-framework reactive runtimes are removed from bundles. Tens of kilobytes like MobX (~17KB) and Vue reactivity can be replaced by native implementations |
| Cross-framework state sharing | Signal-based logic written once can be reused across React, Vue, and Solid with only thin adapters |
| Engine-level optimization | JavaScript engines like V8 and SpiderMonkey can directly understand and optimize Signal graphs, enabling runtime performance improvements |
| Unified learning curve | No need to separately learn different APIs for Redux, MobX, Recoil, Zustand—everything converges on a single standard |
| Web Components integration | Lit (Google) is experimenting with TC39 Signals integration, making reactive state sharing across component boundaries possible |
Disadvantages and Caveats
| Item | Description | Mitigation |
|---|---|---|
effect() not included |
The most commonly used side-effect API is absent from the standard | Implement directly via Signal.subtle.Watcher or delegate to the framework's effect() wrapper |
| Stage 1 uncertainty | The API may continue to change; reaching Stage 4 (final adoption) is expected to take 2–3+ years | Keep usage at experimental level with signal-polyfill rather than using directly in production |
| Mismatch with React | The immutable state philosophy and the mutable reactive cell philosophy are fundamentally different | React projects may have complex adapter layers, so decide the scope of adoption carefully |
| Gap with Svelte | Svelte 5 prioritizes compile-time optimization over runtime Signal objects | In Svelte projects, using Runes ($state, $derived) is recommended over directly using standard Signals |
One more thing worth clarifying about the polyfill dependency period. There's a common misconception that "browser implementations begin only after Stage 4"—in reality, browser vendors begin implementing at Stage 3. This is because Stage 4 itself requires "two or more browser implementations to be complete" as a condition. That doesn't mean you can use it without a polyfill right now, but it does mean browser support could arrive sooner than expected.
The Most Common Mistakes in Practice
1. Adopting Stage 1 proposals directly in production
signal-polyfill is excellent for experimentation and learning, but API changes can arrive at any time. Embedding new Signal.State() directly into product code right now is premature.
2. Assuming effect() is hiding somewhere in the standard
As mentioned above, but worth repeating—it's not there. You should either understand Signal.subtle.Watcher and wrap it yourself, or simply use the effect() provided by your framework.
3. Forcing TC39 Signals into Svelte projects
Svelte 5's Runes are syntax that the compiler transforms, so they operate at a different layer from runtime Signal objects. Forcibly mixing the two can break compiler optimizations.
Closing Thoughts
TC39 Signals is an attempt to rewrite at the language level not the question of "which framework to use," but "where to put reactive logic." It's still Stage 1 and final adoption will take years, but the fact that Angular, Vue, SolidJS, and Preact are already running similar concepts in production—and the SolidJS creator is sitting at the TC39 table—speaks volumes about the credibility of this direction. JavaScript gaining a reactive state primitive is already a fixed destination—it's only a question of how long it takes.
Three steps you can start right now:
-
Install
signal-polyfilland get hands-on with the TC39 spec directly. A singlenpm install signal-polyfillis all it takes, and the official GitHub (github.com/proposal-signals/signal-polyfill) has well-organized runnable examples. -
It's worth looking at how your current framework's Signal implementation (
ref,signal,createSignal) differs from the TC39 standard API using the comparison table above and mapping them out. If you're already using Vue or Angular, you may find the gap is smaller than you thought. -
It helps to pick one piece of framework-independent state logic and rewrite it using only
Signal.StateandSignal.Computed. The goal is not polish but confirming "can this logic be expressed with just the standard API?" You'll get a feel for how thin you can keep the adapter layer.
References
- GitHub - tc39/proposal-signals: A proposal to add signals to JavaScript
- A TC39 Proposal for Signals — EisenbergEffect (Medium)
- GitHub - proposal-signals/signal-polyfill (공식 폴리필)
- What do TC39 Signals mean for you, specifically?
- Standardizing Signals in TC39 by Daniel Ehrenberg — GitNation
- 2026 Frontend Framework War: Signals Won, React Is Living Off Its Ecosystem — DEV Community
- The state of Solid.js in 2026: signals, performance, and growing influence
- Signals in Svelte, Solid, Vue, Angular, Qwik and VanillaJS — JavaScript in Plain English
- State of JavaScript 2025: Front-end Frameworks
- GitHub - transitive-bullshit/ts-reactive-comparison
- Signals – Lit (Web Components)