Skip to main content
Back to Journal
JavaScriptFrameworks

Signals: The State Primitive Taking Over JavaScript Frameworks

Something interesting is happening across the JavaScript framework ecosystem. Solid.js has signals. Preact added signals. Angular shipped signals in version 16. Vue has always had a similar concept with its reactivity system. There is even a TC39 proposal to add signals to the JavaScript language itself. Meanwhile, React is the notable holdout, doubling down on its re-render model.

If every framework except React is converging on the same primitive, it is worth understanding what signals are, why they matter, and what this means for the future of frontend development.

What Are Signals?

A signal is a reactive container for a value. When the value changes, anything that depends on that value updates automatically. That is the entire concept. The power comes from how granular these updates are.

In a signal-based system, there are typically three primitives:

  • Signal: a reactive value container. You can read it and write to it.
  • Computed (also called "derived" or "memo"): a value derived from one or more signals. It recalculates only when its dependencies change.
  • Effect: a side effect that runs when its dependencies change. Used for DOM updates, network requests, logging, etc.

The runtime automatically tracks which computeds and effects depend on which signals. When a signal changes, only the specific computeds and effects that depend on it re-execute. Nothing else runs. This is "fine-grained reactivity."

The Problem with React's Re-render Model

React uses a fundamentally different approach. When state changes in a component, React re-renders that entire component and all of its children. It then diffs the virtual DOM against the previous render to figure out what actually changed in the real DOM.

This is simple to reason about but wasteful. If a component has 100 elements and only one of them depends on the changed state, React still re-renders all 100 elements, diffs them, and then applies the single DOM update. The re-rendering and diffing of the other 99 elements was wasted work.

React mitigates this with useMemo, useCallback, React.memo, and the upcoming compiler (React Forget). But these are all workarounds for a fundamental architectural decision. You are manually opting out of unnecessary work rather than having the framework avoid it in the first place.

Signals flip this model. Instead of "re-render everything, then diff to find what changed," signals say "I know exactly what depends on this value, so I update only that." No diffing needed. No virtual DOM. No wasted re-renders.

Solid.js: The Signal Pioneer

Solid.js, created by Ryan Carniato, was the first modern framework to build entirely on signals. Here is what state management looks like in Solid:

import { createSignal, createEffect, createMemo } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);
  const doubled = createMemo(() => count() * 2);

  createEffect(() => {
    console.log("Count is now:", count());
  });

  return (
    <div>
      <p>Count: {count()}</p>
      <p>Doubled: {doubled()}</p>
      <button onClick={() => setCount(count() + 1)}>
        Increment
      </button>
    </div>
  );
}

Notice something subtle: `count()` is a function call, not a value reference. This is how Solid tracks dependencies. When you call `count()` inside a computed or effect, Solid registers that dependency. When count changes, only the specific DOM text nodes bound to `count()` and `doubled()` update. The button element, the div, the p tags, none of them re-render.

The component function itself runs only once. There is no re-rendering. The JSX is compiled into real DOM creation code, and signals handle all subsequent updates surgically.

Preact Signals

Preact took a different approach. Instead of building a new framework on signals, they added signals as an optional layer on top of their existing React-compatible framework:

import { signal, computed, effect } from "@preact/signals";

const count = signal(0);
const doubled = computed(() => count.value * 2);

function Counter() {
  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => count.value++}>
        Increment
      </button>
    </div>
  );
}

Preact signals use `.value` for access instead of function calls. Signals can be passed directly into JSX, and Preact handles the subscription automatically. The signals can be declared outside of components, which makes shared state trivial without any context providers or state management libraries.

Angular Signals

Angular's adoption of signals (starting in Angular 16, stabilized in Angular 17) is perhaps the most significant endorsement. Angular is an enterprise framework used by massive teams at Google, Microsoft, and countless corporations. Their decision to move from Zone.js-based change detection to signals represents a major shift in how the framework thinks about reactivity:

import { Component, signal, computed, effect } from "@angular/core";

@Component({
  selector: "app-counter",
  template: "
    <div>
      <p>Count: {{ count() }}</p>
      <p>Doubled: {{ doubled() }}</p>
      <button (click)=increment()>Increment</button>
    </div>
  ",
})
export class CounterComponent {
  count = signal(0);
  doubled = computed(() => this.count() * 2);

  constructor() {
    effect(() => {
      console.log("Count changed:", this.count());
    });
  }

  increment() {
    this.count.update((n) => n + 1);
  }
}

Angular signals use function calls like Solid (`count()` instead of `count.value`). The `signal()`, `computed()`, and `effect()` functions mirror the standard signal pattern. This replaces the need for Zone.js monkey-patching and RxJS Observables for simple state management, dramatically simplifying Angular's mental model.

The Same UI, Three Frameworks

Look at the three examples above. The pattern is nearly identical across Solid, Preact, and Angular. Create a signal. Derive values with computed. React to changes with effect. The syntax differs slightly, but the mental model is the same.

Now compare that with React's approach for the same UI:

import { useState, useEffect, useMemo } from "react";

function Counter() {
  const [count, setCount] = useState(0);
  const doubled = useMemo(() => count * 2, [count]);

  useEffect(() => {
    console.log("Count is now:", count);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

The React version requires a dependency array for both useMemo and useEffect. Forget a dependency and you get stale closures. Add an unnecessary dependency and you get wasted recalculations. The dependency array is a manual approximation of what signals track automatically.

When count changes in React, the entire Counter function re-executes. useState, useMemo, and useEffect all run again. React diffs the virtual DOM output. With signals, none of that happens. The count signal notifies its specific subscribers, and only the bound DOM text nodes update.

Performance Implications

For a simple counter, the performance difference does not matter. But scale this to a real application with hundreds of components, complex state trees, and frequent updates. The difference becomes substantial.

In the JS Framework Benchmark, Solid.js consistently outperforms React by 2x to 5x on metrics like creating, updating, and deleting rows in a large table. Angular with signals is significantly faster than Angular with Zone.js. Preact with signals beats Preact without them.

The reason is fundamental: signals eliminate wasted work at the architecture level. React tries to minimize wasted work after the fact through diffing and memoization. The signal approach is inherently more efficient because it never does the wasted work in the first place.

The TC39 Signals Proposal

The most forward-looking development is the TC39 proposal to add signals to the JavaScript language itself. Led by Rob Eisenberg and Daniel Ehrenberg, the proposal defines a standard Signal primitive that all frameworks could build on.

If adopted, this means framework-agnostic reactive state. A signal created with the standard API would work with Solid, Angular, Preact, or any future framework. Library authors could publish reactive utilities that work everywhere. The fragmentation between framework-specific reactivity systems would end.

The proposal is still at Stage 1, so it is years away from shipping in browsers. But the fact that it exists, with backing from multiple framework authors, shows how strong the consensus is that signals are the right primitive for reactive UI.

What This Means for React Developers

React is not adopting signals. The React team is betting on their compiler (React Forget, now called React Compiler) to automatically optimize re-renders. Instead of giving developers a new primitive, they want to make the existing model fast enough that you do not notice the wasted work.

This might work. The React Compiler is impressive technology. But it is swimming against the current. Every other framework has concluded that fine-grained reactivity is the right model, and signals are the right primitive. React is choosing to optimize around the limitations of its model rather than adopting the model that eliminates those limitations.

As a React developer, you do not need to switch frameworks. But you should understand signals because the concepts are spreading everywhere. If you ever work with Angular, Solid, Vue, Preact, or Svelte (which uses a similar compile-time reactivity model), signals are the foundation. And if the TC39 proposal lands, signals become a platform primitive that transcends any single framework.

signalsreactivityangularsolid-jspreactstate-management