All questions

Signals (signal / computed / effect)

Premium

Signals (signal / computed / effect)

A signal is a single reactive value: you read and write it through one function handle, and anything that read it re-runs automatically the moment you write it. This is the core of fine-grained reactivity libraries like SolidJS, Preact Signals, and Vue's ref/computed. You will build three primitives that work together — signal for state, computed for derived values, and effect for side effects — wired by automatic dependency tracking, so callers never declare a dependency list by hand. The key mechanism is a global stack of the observers currently running: any signal read during a run subscribes whoever is on top.

Signature

type Read<T> = () => T;
type Signal<T> = {
  (): T; // read (and subscribe the running observer)
  (next: T): T; // write (and re-run every observer that read it)
};

const signalsComputedEffect: {
  signal<T>(initial: T): Signal<T>; // read/write state handle
  computed<T>(fn: () => T): Read<T>; // lazy, cached derived value
  effect(fn: () => void): () => void; // runs now, re-runs on change; returns dispose()
};

Examples

const { signal, computed, effect } = signalsComputedEffect;

const count = signal(1);
const double = computed(() => count() * 2);
effect(() => console.log('double is', double())); // logs: double is 2

count(5); // logs: double is 10
count(5); // nothing logs — the value did not change
const a = signal(0);
const b = signal(0);
const stop = effect(() => console.log(a())); // logs 0; reads a, never b

b(99); // nothing logs — the effect never read b
a(1); // logs 1
stop(); // dispose the effect
a(2); // nothing logs — the effect is disposed

Notes

  • Read vs write — calling a signal handle with no arguments reads its value; calling it with one argument writes a new value.
  • Automatic tracking — a read inside a running computed or effect subscribes it automatically; there is no dependency array to pass.
  • Lazy and cached — a computed recomputes only when it is read after one of its sources changed; between changes it returns the cached value, and the derived function does not run.
  • Dynamic dependencies — dependencies are re-collected on every run, so a read behind an if is tracked only on the runs where it actually happens.
  • Synchronous — propagation is immediate and synchronous. There are no timers, no microtasks, and no batching to implement.
  • Out of scope — batching multiple writes, glitch-free scheduling across shared computeds, and cleanup functions returned from an effect.

FAQ

How is a signal different from React state?
A signal is a standalone value you read and write through a function handle, and anything that reads it subscribes automatically — so only the exact computeds and effects that used it re-run, with no component tree and no re-render of unrelated code.
Why does a computed read by two effects only recompute once?
A write to its source marks the computed dirty but does not recompute it; the first effect that reads it triggers one recomputation and caches the result, and the second effect reads that cache.
What is automatic dependency tracking?
While an effect or computed runs, it sits on a global current-observer stack, and every signal read during that run subscribes the observer to itself — so dependencies are discovered by running the code rather than declared by hand.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium