Skip to content

Step 1 of 5 · Reading · ~4 min

Read

Hooks & State

Signals and Fine-Grained Reactivity

Hooks answer "what changed?" by running the whole component again and diffing the result. Signals answer it differently: each piece of state keeps track of who reads it, and a write touches only those readers, never a whole component. Solid, Vue 3 refs, Svelte 5 runes, Angular Signals and Preact Signals all work this way. After this lesson your program is a small signals runtime: values, derived values, effects that find their own dependencies, and batched writes.

Three primitives

  • A signal holds a value. Anyone can read it; writing it is the only way anything changes.
  • A computed value is derived from other signals or computed values. Nobody writes it; it is always the current result of its formula.
  • An effect runs once when it is created and again whenever something it read has changed. It is where output happens: updating a DOM node, logging, a request.

Dependencies are discovered, not declared

A hook's dependency array is a list you write and can get wrong. A signal runtime builds the list by watching. While an effect runs, every signal or computed value it reads is recorded together with the value it saw. That has a consequence a written list cannot express: the dependencies can change from run to run.

effect:  flag ? a : b

This reads flag, and then only one of a and b. While flag is true, the effect depends on flag and a; writing b does nothing. When flag flips and the effect re-runs, it now depends on flag and b, and writing a does nothing. An effect never runs for state it did not look at.

Deciding whether to re-run

After a write, look at every effect, in creation order. If at least one of the values it read last time is now different, re-run it, and replace its recorded reads with the new run's. Otherwise leave it alone.

Computed values are re-evaluated when read, so this check gives you a free cutoff. Suppose big is the computed value n > 10 and an effect reads big. Writing n from 5 to 7 changes n but not big, so the effect stays quiet. It runs when n crosses 10 in either direction, and only then. A derived value that did not change stops the update from travelling further.

An effect runs at most once per flush, even when several of its reads changed. If an effect reads both a and double, a computed a * 2, one write to a changes both, and still you get one run, seeing consistent values.

Batching

Without batching, every write flushes at once. Two writes in a row mean two runs, and the first sees a half-updated world: the new a with the old b. A batch applies all its writes first and flushes once at the end, so each affected effect runs once with the final values. Writing a value equal to the current one, or writing it back to where it started inside one batch, makes nothing stale and runs nothing. Frameworks wrap event handlers in an implicit batch for exactly this reason.

Why not always diff?

A virtual DOM does less bookkeeping at write time and more work at render time. Signals pay at write time (recording reads) and then skip the render of everything that did not change; the compiler can wire a signal straight to the one DOM node that shows it. It is the same question as lesson 5, answered without a tree to compare.

The script you will run

SIGNAL a = 2             a named integer
COMPUTED sum = a + b     a derived value; formulas are one operand, "x op y" (+ - * > < ==) or "c ? x : y"
EFFECT show = sum        runs now and prints its value
SET a = 10               write, then flush
BATCH ... END            writes inside are applied together, flushed at END

Common mistakes

  • Using a fixed list of dependencies instead of the reads of the latest run, so a ternary keeps depending on the branch it no longer takes.
  • Reading both branches of a ternary when recording reads.
  • Running an effect once per changed read, instead of once per flush.
  • Forgetting to replace the recorded reads after a re-run, which leaves the effect watching stale values.
  • Running effects whose read values are equal to what they saw, such as after writing the same number again.

Your exercise

The starter reads the script, stores signals and formulas, and handles SET, BATCH and END. You write evaluate, which computes a formula and records the reads, run_effect, and flush.

Up nextPerformance OptimizationProduction

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…