
What Are Signals? The Reactive Primitive Taking Over Frontend
Reactivity has always been at the heart of frontend frameworks. Over the years, we have seen different approaches: dirty checking, getters and setters, virtual DOM diffing. Now, a new primitive is quietly taking over: signals. You are going to hear a lot more about them.
What Are Signals?
A signal is a container around a value that notifies interested code when that value changes. It is the simplest possible reactive primitive: you read a value, you write a value, and the framework handles the rest.
Here is what a signal looks like in practice:
const count = signal(0);
console.log(count.value); // 0
count.value = 1;
console.log(count.value); // 1
Nothing surprising so far. The magic happens when you derive values from signals or run side effects in response to changes.
const count = signal(0);
const doubled = computed(() => count.value * 2);
effect(() => {
console.log(`Count is ${count.value}, doubled is ${doubled.value}`);
});
// Logs: Count is 0, doubled is 0
count.value = 5;
// Logs: Count is 5, doubled is 10
A computed derives a new signal from existing ones. It re-evaluates only when its dependencies change. An effect runs a function and automatically re-runs it whenever the signals it reads change. There is no manual subscription, no dependency array, no selector function. You read a signal, and the framework tracks it.
How It Differs From Virtual DOM Reactivity
Most frameworks today – React, Vue 3, Preact – use a top-down re-render model. When state changes, the component function runs again, producing new virtual DOM, which gets diffed against the old one. The framework then patches the real DOM.
This works, but it is broad. A state change in one corner of a component causes the whole component to re-evaluate, even if only a tiny piece of the UI actually depends on that state.
Signals flip this model. Instead of re-running the entire component, signals update only the specific parts of the DOM that depend on the changed value:
function Counter() {
const count = signal(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => count.value++}>Increment</button>
</div>
);
}
When count changes, only the text node that displays it gets updated. The button and the wrapping div are untouched. There is no diff, no reconciliation, no component-level re-render. The update is a straight line from the signal to the DOM node.
Signals Across the Ecosystem
Signals are not tied to any single framework. The idea has been spreading rapidly:
-
Solid.js was the first major framework to fully embrace signals. Its compiler transforms JSX into direct DOM bindings against signals, skipping the virtual DOM entirely.
-
Vue 3’s
refandreactiveare already signal-like. Vue’s upcoming Vapor Mode pushes this further by compiling templates into signal-based updates without a virtual DOM. -
Preact introduced
@preact/signalsas an add-on that works alongside Preact’s existing component model. You can adopt signals incrementally in existing Preact apps. -
Angular has made signals a core part of its reactivity model starting with Angular 16, replacing
Zone.js-based change detection. -
Svelte 5 introduced runes, which are essentially signals under a different name.
The TC39 committee is even exploring a Signal proposal to standardize signals as a JavaScript language feature.
Composing Signals
One of the biggest advantages of signals is how well they compose. You can build complex state from simple primitives:
const items = signal([]);
const filter = signal('all');
const filteredItems = computed(() => {
switch (filter.value) {
case 'active':
return items.value.filter(item => !item.done);
case 'completed':
return items.value.filter(item => item.done);
default:
return items.value;
}
});
const stats = computed(() => ({
total: items.value.length,
active: items.value.filter(item => !item.done).length,
completed: items.value.filter(item => item.done).length,
}));
Every computed value automatically stays in sync. Change items or filter, and both filteredItems and stats update. No orchestration is needed.
Compare this to the equivalent in React, where you would need useMemo with explicit dependency arrays, or to Vue 2, where computed properties were defined inside a component configuration object. Signals let you define derived state anywhere, not just inside components.
Deriving Async State
Signals work well with asynchronous data too. Some libraries provide resource or async constructs built on top of signals:
const userId = signal(1);
const user = resource(() => fetchUser(userId.value));
When userId changes, the resource automatically re-fetches. The component template can read user.loading, user.error, and user.value to show the right UI. No useEffect, no watch, no manual lifecycle hooks.
You can even chain async resources:
const userId = signal(1);
const user = resource(() => fetchUser(userId.value));
const posts = resource(() => fetchPosts(user.value.id));
Change userId to 2, and both fetches cascade automatically.
Trade-Offs and Gotchas
Signals are not a silver bullet. There are things to watch out for:
Finding the right granularity. If you turn every piece of local state into a signal, you end up with many tiny reactive cells that can be harder to reason about than a single useState call. Use signals for state that flows across your app or for values that change independently of the component lifecycle.
Accidental untracking. Most signal libraries require you to explicitly unwrap signal values. In some frameworks, passing signal.value instead of signal to a child component can break reactivity. The tooling is improving, but it is an extra mental model to learn.
Debugging. When a computed value gives the wrong result, a top-down data flow is easier to trace than a web of signal dependencies. Good devtools help, but they are still catching up across frameworks.
Not a full replacement for the virtual DOM in every case. For content-heavy sites with infrequent updates, the virtual DOM model is perfectly fine. Signals shine in highly interactive interfaces where updates are frequent and targeted.
When Should You Care?
You should pay attention to signals if:
- You are building dashboards, real-time UIs, or highly interactive tools.
- You are frustrated with
useMemo,useCallback, and dependency array bugs in React. - You want finer control over what re-renders without manual
React.memoorshouldComponentUpdate. - You are starting a new project and want to pick a future-proof reactivity model.
If you are maintaining a large existing codebase, gradual adoption is the way to go. Preact’s @preact/signals and Vue’s Vapor Mode are both designed so you can add signals to individual components without rewriting everything.
Final Thoughts
Signals represent a shift toward pull-based, fine-grained reactivity. Instead of telling the framework “my state changed, please figure out what to update,” you tell it exactly what depends on what, and it handles the updates for you.
The fact that Solid, Vue, Preact, Angular, and Svelte are all converging on this model says something. Signals are not a framework feature anymore. They are becoming the default way to think about reactivity on the frontend.
If you have not tried signals yet, start with a small side project using Solid.js or add @preact/signals to an existing Preact app. Once you feel how direct and predictable the updates are, it is hard to go back.