Skip to content
fearchitect
State & Data

Signals & Fine-Grained Reactivity

Observable values that re-run only their exact dependents.

By Abas TurabliReviewed

Summary

A signal is a reactive cell: read it and you subscribe; write it and only the computations that read it re-run. No component re-renders, no VDOM diff — only the precise DOM nodes or derived values that depend on changed state update. SolidJS, Preact, Angular, and Vue all ship this model today.

Jump to the interview angle

Signal

A signal wraps a value and tracks every computation that reads it. When the value changes, those computations — and nothing else — are notified and re-evaluated.

Compare that to React: a setState call schedules the whole component to re-render, and React walks the VDOM tree to diff what changed. Fine-grained reactivity skips both steps — the dependency graph built at runtime is the schedule.

Three primitives appear in every implementation: a state signal (readable/writable), a computed (derived, memoized, lazy), and an effect (re-runs on dependency change). SolidJS uses createSignal, createMemo, and createEffect. Preact uses signal(), computed(), and effect(). Vue exposes the same model through ref(), computed(), and watchEffect().

A write to price or qty propagates only to computed:total and its two dependents — nothing else re-runs.

How signals track dependencies

  1. 1

    Push observer onto stack

    When a computed or effect starts running, the runtime pushes it onto a global current-observer stack.

  2. 2

    Signal records the subscriber

    Every signal get inside that function sees the stack and adds the running observer to its subscriber list.

  3. 3

    Write marks dependents dirty

    When a signal value is set, it walks its subscriber list and marks each observer dirty. Effects schedule immediately; computeds wait until read.

  4. 4

    Lazy vs. eager execution

    Computed values are lazy — they re-run only when read after being dirtied. Effects are eager — they fire after the write. A computed no one reads never runs.

SolidJS — createSignal with derived createMemo

The component function runs exactly once. JSX compiles to fine-grained effects that write directly to DOM nodes — no VDOM, no re-render.

SolidJS counter with derived memotsx
import { createSignal, createMemo, createEffect } from "solid-js";

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

  // Runs only when count() changes — not on every render.
  createEffect(() => console.log("count:", count()));

  return (
    <button onClick={() => setCount(count() + 1)}>
      {count()} × 2 = {doubled()}
    </button>
  );
}

createSignal returns a getter/setter tuple. createMemo subscribes to count() and caches the result. The component function never re-runs.

Preact Signals — reactive state outside any component

Signals are plain objects. computed and effect auto-subscribe by reading .value during their first run — no component or hook required.

Preact Signals — signal and computedts
import { signal, computed, effect } from "@preact/signals";

const price = signal(100);
const qty = signal(3);
const total = computed(() => price.value * qty.value);

// Logs whenever price or qty changes.
effect(() => console.log("total:", total.value));

price.value = 120; // logs "total: 360"

Signals are plain objects — no component needed. computed and effect auto-subscribe by reading .value during their first run.

Signals vs. component-based reactivity

Pros

  • Only changed dependents update — no VDOM diff, no full component re-render.
  • Derived state is memoized automatically; no manual `useMemo` needed.
  • Effects subscribe precisely — no stale-closure or missing-dependency bugs.
  • Scales to large reactive graphs without cascading re-renders.
  • TC39 Stage 1 proposal may land `Signal.State`/`Signal.Computed` in JS itself.

Cons

  • Reading a signal outside a tracked scope silently skips subscription.
  • Circular dependencies between computeds cause infinite loops at runtime.
  • Debugging reactive graphs is harder than stepping through a call stack.
  • React's ecosystem — hooks, RSC, Suspense — doesn't map to signals without shims.

React's answer: React Compiler

React Compiler (1.0 stable, October 2025; works with React 17+) auto-inserts useMemo/useCallback at compile time to reduce re-renders — targeting the same problem without changing React's component model.

Interview angle

Interviewers want to know the contrast with React's model. Be concrete: signals track dependencies at the expression level and skip VDOM; React re-renders the component and diffs.

Soundbite: "Signals make the dependency graph explicit at runtime — only the expressions that read a changed value re-run, with no diff needed."

Key terms

signal
A reactive cell: reads subscribe the caller; writes notify all current subscribers.
computed
A derived, read-only signal that lazily re-evaluates when any dependency changes.
effect
A side-effectful callback that re-runs eagerly whenever its signal dependencies change.
fine-grained reactivity
Updates propagate to individual expressions or DOM nodes, not entire components.
TC39 Signals proposal
A Stage 1 proposal to add `Signal.State` and `Signal.Computed` as native JS primitives.

Further reading

Search fearchitect

Jump to a topic, mode, or action.