Signals in JavaScript 2026: The Future of Web Reactivity
Hello HaWkers, one of the most anticipated TC39 proposals is advancing in 2026: native Signals in JavaScript. This feature promises to unify how frameworks handle reactivity and could fundamentally change the way we build web applications.
Let's explore what Signals are, how they work, and their impact on the ecosystem.
What Are Signals
Fundamental Concept
// Signal is a reactive container for values
// Simple analogy
const traditionalVariable = 5; // Static value
// If it changes, nobody knows
// Signal
const count = signal(5); // Reactive value
// When it changes, dependents are automatically notifiedHow They Work
// Conceptual implementation (not the final TC39 API)
// 1. Create a signal
const count = signal(0);
// 2. Read the value
console.log(count.value); // 0
// 3. Write a value
count.value = 1; // Notifies dependents
// 4. Computed (derived from other signals)
const doubled = computed(() => count.value * 2);
// doubled.value is always count.value * 2
// Automatically recalculates when count changes
// 5. Effect (reactive side effect)
effect(() => {
console.log(`Count is now: ${count.value}`);
});
// Executes whenever count changes
Why Signals Matter
The Current Problem
// React: entire component re-render
function Counter() {
const [count, setCount] = useState(0);
// When count changes:
// 1. Counter function executes again
// 2. Virtual DOM is created
// 3. Diff with previous DOM
// 4. Patches applied
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {count * 2}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
);
}
// Even though only count changed, everything re-renders// Signals: granular update
function Counter() {
const count = signal(0);
const doubled = computed(() => count.value * 2);
// When count changes:
// 1. Only the text using count.value updates
// 2. Computed doubled recalculates
// 3. Only the text using doubled updates
// 4. No component re-render
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count.value++}>
Increment
</button>
</div>
);
}
// Surgical update, no overheadPerformance Comparison
// Conceptual benchmark
const performanceComparison = {
react: {
update: 'Component re-render',
complexity: 'O(tree size)',
overhead: 'Virtual DOM diff',
ideal: 'UIs with many coordinated changes'
},
signals: {
update: 'Direct DOM mutation',
complexity: 'O(1) per signal',
overhead: 'Dependency tracking',
ideal: 'Frequent and granular updates'
},
realWorld: {
smallApp: 'Negligible difference',
largeApp: 'Signals 2-10x faster',
animations: 'Signals much superior',
forms: 'Signals much superior'
}
};
Frameworks Already Using Signals
SolidJS
// SolidJS: Signals from the beginning
import { createSignal, createEffect, createMemo } from 'solid-js';
function Counter() {
// Primitive signal
const [count, setCount] = createSignal(0);
// Computed (memo)
const doubled = createMemo(() => count() * 2);
// Effect
createEffect(() => {
console.log('Count changed:', count());
});
return (
<div>
<p>Count: {count()}</p>
<p>Doubled: {doubled()}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
);
}
// Note: count() is a function, not .value
// SolidJS uses functions for trackingVue 3
// Vue 3: Composition API with Signal-based reactivity
import { ref, computed, watchEffect } from 'vue';
// ref is essentially a Signal
const count = ref(0);
// computed is a derived Signal
const doubled = computed(() => count.value * 2);
// watchEffect is an Effect
watchEffect(() => {
console.log('Count changed:', count.value);
});
// In Vue template
// <template>
// <p>Count: {{ count }}</p>
// <p>Doubled: {{ doubled }}</p>
// <button @click="count++">Increment</button>
// </template>Angular
// Angular 17+: Official Signals
import { signal, computed, effect } from '@angular/core';
@Component({
template: `
<p>Count: {{ count() }}</p>
<p>Doubled: {{ doubled() }}</p>
<button (click)="increment()">Increment</button>
`
})
export class CounterComponent {
// Signal
count = signal(0);
// Computed
doubled = computed(() => this.count() * 2);
constructor() {
// Effect
effect(() => {
console.log('Count changed:', this.count());
});
}
increment() {
this.count.update(c => c + 1);
// or: this.count.set(this.count() + 1);
}
}Preact Signals
// Preact Signals: works in React too!
import { signal, computed, effect } from '@preact/signals-react';
// Global signals (outside the component)
const count = signal(0);
const doubled = computed(() => count.value * 2);
function Counter() {
// No hooks! Signal is global and reactive
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => count.value++}>
Increment
</button>
</div>
);
}
// The component DOESN'T re-render when count changes
// Only the text is updated directly
The TC39 Proposal
Status in 2026
// TC39 Signals Proposal
const tc39Proposal = {
stage: 'Stage 2 (draft)',
champions: ['Rob Eisenberg (Angular)', 'Daniel Ehrenberg'],
goal: 'Standard primitives for reactivity',
apis: {
// Basic Signal
'Signal.State': 'Mutable reactive value',
'Signal.Computed': 'Derived value (read-only)',
'Signal.subtle.Watch': 'Low-level API for effects',
},
notIncluded: {
effect: 'Frameworks implement',
batch: 'Frameworks implement',
rendering: 'Frameworks implement'
},
philosophy: 'Interoperability, not replacement'
};Proposed API
// TC39 API (subject to changes)
// Signal.State - mutable value
const count = new Signal.State(0);
count.get(); // 0
count.set(1);
count.get(); // 1
// Signal.Computed - derived value
const doubled = new Signal.Computed(() => count.get() * 2);
doubled.get(); // 2
// doubled.set() doesn't exist - it's read-only
// Signal.subtle.Watch - for frameworks
const watcher = new Signal.subtle.Watch(() => {
// Called when dependencies change
console.log('Something changed');
});
// Frameworks use this to implement effectsWhy Standardize
// The problem of current fragmentation
const currentFragmentation = {
solidjs: 'createSignal() returns tuple',
vue: 'ref() uses .value',
angular: 'signal() uses get/set functions',
preact: 'signal() uses .value',
svelte5: 'Runes with $state',
qwik: 'useSignal() with .value'
};
// With TC39 Signals
const futureInterop = {
sharedPrimitive: 'Signal.State and Signal.Computed',
frameworks: 'Thin wrappers over standard primitives',
libraries: 'Can be framework-agnostic',
benefit: 'Share reactive code between frameworks'
};
Signals vs useState
When to Use Each
// React with useState (current standard)
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [errors, setErrors] = useState({});
// Each setState causes re-render
return (
<form>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<input
value={email}
onChange={e => setEmail(e.target.value)}
/>
{/* Re-renders everything on each keystroke */}
</form>
);
}
// With Signals (e.g., Preact Signals in React)
const name = signal('');
const email = signal('');
const errors = signal({});
function Form() {
// Component doesn't re-render!
return (
<form>
<input
value={name}
onInput={e => name.value = e.target.value}
/>
<input
value={email}
onInput={e => email.value = e.target.value}
/>
{/* Granular updates, no re-render */}
</form>
);
}Trade-offs
// When Signals shine
const signalsIdealFor = [
'Forms with many fields',
'Animations and transitions',
'High-frequency updates',
'Shared state between components',
'Apps with lots of interactivity'
];
// When useState is sufficient
const useStateIdealFor = [
'Simple local state',
'UIs with few changes',
'When re-render is cheap',
'When you need effects on re-render'
];
// Considerations
const considerations = {
devex: 'Signals require different mental model',
debugging: 'useState easier to debug',
ecosystem: 'React ecosystem assumes useState',
future: 'React may add native signals'
};
Implementing Signals Today
In React
// Option 1: Preact Signals
npm install @preact/signals-react
import { signal, computed } from '@preact/signals-react';
// Option 2: Jotai (signal-like)
npm install jotai
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const [doubled] = useAtom(doubledAtom);
// ...
}
// Option 3: Legend State
npm install @legendapp/state @legendapp/state/react
import { observable } from '@legendapp/state';
import { observer } from '@legendapp/state/react';
const state = observable({ count: 0 });
const Counter = observer(function Counter() {
return <div>{state.count.get()}</div>;
});In Vue
// Vue 3 already has signals (refs)
import { ref, computed, watch } from 'vue';
// Reactive composable
function useCounter() {
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubled, increment };
}
// Usage in component
export default {
setup() {
const { count, doubled, increment } = useCounter();
return { count, doubled, increment };
}
};In Svelte 5
// Svelte 5 Runes (signals)
<script>
// $state is a signal
let count = $state(0);
// $derived is a computed
let doubled = $derived(count * 2);
// $effect is an effect
$effect(() => {
console.log('Count is', count);
});
function increment() {
count++; // That simple!
}
</script>
<button onclick={increment}>
Count: {count}
</button>
<p>Doubled: {doubled}</p>
The Future With TC39 Signals
2027+ Scenario
// Possible future with native Signals
// Native JavaScript (no imports!)
const count = new Signal.State(0);
const doubled = new Signal.Computed(() => count.get() * 2);
// React could adopt
function Counter() {
// use() hook for signals
const countValue = use(count);
return <p>Count: {countValue}</p>;
}
// Or even native JSX
<p>Count: {count}</p> // Auto-unwrap in JSX
// Framework-agnostic libraries
class ReactiveDatePicker {
selectedDate = new Signal.State(null);
formattedDate = new Signal.Computed(() =>
this.selectedDate.get()?.toLocaleDateString()
);
// Works in React, Vue, Angular, Svelte...
}Ecosystem Impact
// What changes with native Signals
const ecosystemImpact = {
frameworks: {
change: 'Thin wrappers over native primitives',
benefit: 'Less code, more performance',
risk: 'Less differentiation between frameworks'
},
libraries: {
change: 'Can be framework-agnostic',
benefit: 'Write once, use anywhere',
example: 'Form library that works in React and Vue'
},
learning: {
change: 'Signal concept becomes fundamental',
benefit: 'Knowledge transfer between frameworks',
requirement: 'Understanding reactivity becomes essential'
},
performance: {
change: 'Optimizations in JavaScript engine',
benefit: 'Signals faster than any userland implementation',
expectation: 'V8, SpiderMonkey optimize for signals'
}
};Conclusion
Signals represent a fundamental shift in how we think about reactivity on the web. While React's re-render model dominated the last decade, signals offer a more granular and performant alternative.
What to do now:
- Understand the concept: Even without using them, understand how signals work
- Experiment: Try Preact Signals, Vue 3, or Angular 17+
- Follow TC39: The proposal is advancing
- Don't abandon React: Signals can complement, not replace
The future seems to converge on signals as the standard reactivity primitive. Frameworks will continue to exist, but with a common foundation that facilitates interoperability and learning.
To understand more about the current JavaScript ecosystem, read: VoidZero in 2026.

