Why Signals Are the Future of Reactivity

Team 6 min read

#reactivity

#signals

#frontend

#performance

#architecture

Reactivity is at the heart of modern UI. For years, the common approach has been component-driven updates: change some state, rerun a render function, and reconcile the DOM. It works, but it can be wasteful. As apps scale, even memoized renders and virtual DOM diffing incur overhead. Enter signals: a fine-grained, dependency-tracking model that updates only what actually changed—no component-wide re-renders required.

This article explains what signals are, how they differ from today’s mainstream patterns, and why they’re rapidly becoming the foundation of the next generation of reactive UI.

What is a signal?

A signal is a container for a value that:

  • Notifies only the code that depends on it when it changes.
  • Tracks dependencies automatically when read.
  • Propagates changes synchronously and predictably through a graph of derived values and effects.

In short, signals deliver fine-grained reactivity: they surgically update what needs updating—no more, no less.

Core concepts you’ll see across implementations:

  • Writable signal: holds a value, supports get/read and set/write.
  • Computed/derived signal: read-only signal computed from other signals.
  • Effect: a reaction that runs when its dependencies change (e.g., update DOM, log, fetch).

Why signals beat component-wide re-renders

  • Precision: Only the exact text node, style, or computation that depends on the changed value updates.
  • Predictability: Dependency graphs and topologically ordered updates greatly reduce glitches and stale reads.
  • Performance: Fewer renders, less diffing, and fewer DOM operations—especially visible on large lists and complex views.
  • Composability: Derived state lives as first-class reactive values, not hidden inside render cycles.
  • Portability: Signals are a simple primitive that can power different frameworks or interop layers.

Signals across frameworks

The idea is converging across the ecosystem, even if APIs differ slightly.

SolidJS

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

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

createEffect(() => {
  console.log("doubled is", doubled());
});

batch(() => {
  setCount(1);
  setCount(2); // effects run once after the batch
});

Angular (v17+)

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

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

effect(() => {
  console.log("doubled is", doubled());
});

count.set(1);
count.update(v => v + 1);

In templates, reads are automatic and granular; Angular updates only bindings that depend on the changed signals.

Preact Signals

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

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

effect(() => {
  console.log("doubled is", doubled.value);
});

batch(() => {
  count.value = 1;
  count.value = 2;
});

Using Preact Signals in React apps:

import { useSignal } from "@preact/signals-react";

export function Counter() {
  const count = useSignal(0);
  return (
    <button onClick={() => count.value++}>
      {count.value}
    </button>
  );
}

Qwik

Qwik uses signals to support resumability. State is serialized on the server and resumed on the client, and signals wake only the parts of the UI that need to respond.

Vue

Vue’s refs and reactivity system are conceptually similar: ref() creates reactive values, computed() derives values, and effects are tracked. The core difference is implementation details (proxy-based tracking vs explicit getters/setters), but the fine-grained intent aligns.

How signals change UI architecture

  • State as data, not as rerenders: UI code reads from signals directly; no need to “rerun” an entire render function to compute derived values.
  • Derived state is explicit: computed/memo signals hold derived values that are always in sync.
  • Effects are side-effects only: Rendering, subscriptions, and I/O move into effects with clear lifecycles.
  • Fewer global invalidations: No need to bubble changes through contexts or prop chains when readers subscribe to signals directly.

Async workflows with signals

You can represent async state as signals to model loading, value, and error explicitly.

Solid example:

import { createSignal, createResource } from "solid-js";

const [userId, setUserId] = createSignal("1");
const [user] = createResource(userId, async id => {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error("Failed");
  return res.json();
});

// user.state: "unresolved" | "pending" | "ready" | "errored"

Preact Signals pattern:

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

const userId = signal("1");
const user = signal<{ name: string } | null>(null);
const loading = signal(false);
const error = signal<Error | null>(null);

async function loadUser(id: string) {
  batch(() => {
    loading.value = true;
    error.value = null;
  });
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error("Failed");
    user.value = await res.json();
  } catch (e) {
    error.value = e as Error;
  } finally {
    loading.value = false;
  }
}

effect(() => {
  loadUser(userId.value);
});

const greeting = computed(() =>
  user.value ? `Hello, ${user.value.name}` : "Loading..."
);

Batching, scheduling, and consistency

Most signal libraries:

  • Batch updates to collapse multiple writes into a single flush.
  • Guarantee glitch-free updates by scheduling derived computations in topological order.
  • Provide stable read semantics during a flush (no tearing).

These properties make state changes predictable, even under heavy interaction.

Memory and correctness considerations

  • Dispose effects: If your framework does not auto-clean, ensure you dispose/unsubscribe to avoid memory leaks.
  • Avoid hidden reads in effects: Keep computed values pure and side-effects in effects for clarity.
  • Model async carefully: Represent loading/error/value explicitly to avoid race conditions. Prefer cancelation or versioning for in-flight requests.
  • Keep graphs understandable: Over-fragmenting state can create a tangle. Group cohesive data, derive the rest.

Where signals fit with Astro

Astro’s island architecture ships little or no JavaScript by default and hydrates only interactive islands on the client. Signals pair well with this:

  • Inside an island (Solid, Preact, Angular), signals ensure only the minimal DOM updates occur.
  • You can scope interactivity to small islands and still get extremely responsive updates.
  • Combined with partial hydration or resumability (e.g., Qwik), signals further reduce work on the client.

Result: islands that wake quickly, ship less JavaScript, and perform fewer updates.

Migrating incrementally

  • Start local: Replace a few derived-calculation hotspots with computed signals.
  • Bridge libraries: Use Preact Signals with React components or integrate Angular Signals within a single route or feature module.
  • Keep render code simple: Move logic from render functions into computed signals. Renders become straightforward reads.
  • Measure: Track paint times, scripting cost, and interaction latency. Signals tend to shine under interaction-heavy workloads.

The road ahead

Signals are not a fad—they are a convergent solution to long-standing problems with broad framework buy-in. Whether through Solid’s fine-grained model, Angular’s first-class signals, Preact Signals’ interop, or Qwik’s resumable architecture, the direction is clear: target only what changed, when it changed.

If you’re building interactive UI in 2025, learning signals will pay dividends in performance, clarity, and maintainability.