Skip to content

Signals and computed vs useState and derived values

Angular reruns only what read the changed signal, when read; React re-runs the component and derives values during render, so computed() becomes plain code.

Predict first

Before you read: predict

ExamHall's Angular proctor console shows a sitting's remaining time in a badge:

The proctor keeps the badge's panel closed, so nothing reads remaining, while the timer calls now.set(1), now.set(2) and so on up to now.set(30). She then opens the panel, and its template reads remaining() twice: once for the badge's text and once for its aria-label.

How many times has formatLeft run?

Code for this question
const now = signal(0);   // seconds since the sitting started; a timer sets itconst remaining = computed(() => formatLeft(5400 - now()));
Pick one

The 30-second version

A signal is a cell that knows its readers. Nothing runs on set; the function runs on the next read, and the value is cached until a signal it read changes. React has no cells: a state change re-runs the whole component, so a derived value is just a variable computed in the body. linkedSignal's reset becomes a key.

Angular reruns only what read the changed signal, when read; React re-runs the component and derives values during render, so computed() becomes plain code.

How it works

Step 1 of 12

A signal knows its readers. Reading count() inside a computed records a dependency; set marks those readers stale and runs nothing.

Analogy: A golf tournament's scoring tent

Each player's total is kept by one scorer, and a big board on the clubhouse is repainted for TV.

At the tournament In the code
a scorer adds up a player's card only when a reporter asks, and writes the total on a slip computed() runs on read (step 2)
asked again with no new hole posted, she hands over the same slip cached (step 3)
a new score on another player's card is not her business only readers rerun (step 5)
the clubhouse board is repainted in full whenever any score changes React re-runs the component (step 6)
the "featured group" shown on TV defaults to the leaders; the producer may switch it, and the next round resets it to the new leaders linkedSignal, or a key (steps 10–11)

Breaks down when: a scorer remembers who asked her; a computed keeps no list of its readers' questions, only of the signals it read. Wrong inference: "a scorer re-adds whenever a score is posted".

Contrasting case: an effect() (a commentator who speaks on every new score) runs after a change by itself; a computed waits to be read.

In real terms: Angular tracks reads per value and recomputes on demand; React re-runs the component and you compute in the body, adding useMemo only for costly work and a key to reset state.

Examples, run for real

4.1 ExamHall: EXH-3490 and MR !3790

The React port of the badge (EXH-3490) translated computed() as state plus an effect that copies the new time into it. MR !3790 computes the value in the body (step 7):

// EXH-3490, the port
function RemainingBadge({ now }) {
  const [remaining, setRemaining] = useState(formatLeft(5400 - now));
  useEffect(() => setRemaining(formatLeft(5400 - now)), [now]);
  return <span>{remaining}</span>;
}

// MR !3790
function RemainingBadge({ now }) {
  const remaining = formatLeft(5400 - now);
  return <span>{remaining}</span>;
}
4.1 EXH-3490 port (state + effect): 3 ticks · commits per tick 2, 2, 2 · commits showing the previous time 3
4.1 MR !3790 (computed in the body): 3 ticks · commits per tick 1, 1, 1 · commits showing the previous time 0

The port shows the time one commit late after every tick and commits twice per tick; computing in the body shows the right time in the one commit (steps 6–7).

4.2 Ticketing: useMemo for a costly count

A seat map counts free seats over 2,000 seats, and a search box sits beside it. countFree is wrapped once in useMemo and once left in the body (step 8):

const free = useMemo(() => countFree(seats), [seats]);   // with useMemo
const free = countFree(seats);                           // without
4.2 2,000 seats · 4 keys in the search box · countFree calls after the first render: with useMemo 0, without 4 · map shows 1333 free

useMemo skips the count while seats is the same array; it would not help a cheap value, and React may drop the cache (step 9).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

A ticket site's seat-hold banner shows how long a held seat stays reserved. The Angular version:

The React port keeps the filter box in its own component:

function HoldBanner({ now }) {
  const left = formatLeft(600 - now);
  return (
    <>
      <SeatFilter />
      <p>{left} left</p>
    </>
  );
}

function SeatFilter() {
  const [filter, setFilter] = useState('');
  return <input value={filter} onChange={(e) => setFilter(e.target.value)} />;
}

With now at 120 in both, a fan clicks into the filter box and types row, one key at a time. Angular checks the view when she clicks (the box's (focus) handler runs) and after each key, which calls filter.set(…); each check reads left() once.

After the banner's first display, which version runs formatLeft again, and when?

Code for this question
const now = signal(0);       // seconds since the hold startedconst filter = signal('');   // the seat filter boxconst left = computed(() => formatLeft(600 - now()));
Pick one

Question 2 of 7

A payments ledger's Angular review screen flags large transfers:

Each check of the view reads badge() twice: for its text and for its colour. The view is checked once at first display, then once after each of 5 edits that set amountCents to 5200, 7000, 9900, 12000 and 150000.

How many times does badgeFor run in all?

Code for this question
const amountCents = signal(4500);const isLarge = computed(() => amountCents() > 100000);const badge = computed(() => badgeFor(isLarge()));
Pick one

Question 3 of 7

A chat app's channel picker starts on the first channel of the list it is given. The Angular version:

The React port keys the picker by its list:

function ChannelBar({ channels }) {
  return <ChannelPicker key={channels.join()} channels={channels} />;
}

function ChannelPicker({ channels }) {
  const [selected, setSelected] = useState(channels[0]);
  return (
    <select value={selected} onChange={(e) => setSelected(e.target.value)}>
      {channels.map((c) => <option key={c}>{c}</option>)}
    </select>
  );
}

In both, the user picks random. The list then reloads from the server as ['announcements', 'general', 'random', 'help'].

Which channel does each version show selected?

Code for this question
const channels = signal(['general', 'random', 'help']);const selected = linkedSignal(() => channels()[0]);
Pick one

Question 4 of 7

A video app's "Up next" panel shows how many minutes of episodes are queued and how far into the current episode the viewer is. The Angular version:

Its template reads total() and position() once per check. The React port, with one line still to write:

function UpNext({ queue, position }) {
  // the line that gives total
  return <p>{queue.length} queued · {total} min · {position} s in</p>;
}

The player sets position to 1, 2, 3, 4 and 5, then adds one episode to the queue (a new array). Angular checks the view after each change; React's parent renders UpNext again after each one.

Which line shows the new total in the same commit as the new queue, and runs sumDurations no more often than Angular runs total's function after the first display?

Code for this question
const queue = signal(loadQueue());   // 2,000 episodesconst position = signal(0);          // seconds into the current episodeconst total = computed(() => sumDurations(queue()));
Pick one

Question 5 of 7

A teammate who knows Angular well says: "useState is React's signal, useMemo is computed(), and I'll port our linkedSignals with useEffect." Correct each claim.

Three sentences, one per claim.

Question 6 of 7

A ticket site's Angular seat-hold timer shows minutes until the last minute, then seconds:

A timer sets secondsLeft once a second, 65 times (119 down to 55); with the set to 60 it also sets lastMinute to true. minutesLeft stays 2. The view is checked at first display and after each second, and each check reads label() twice (its text and its aria-label).

How many times does label's function run in all?

Code for this question
const lastMinute = signal(false);const minutesLeft = signal(2);const secondsLeft = signal(120);const label = computed(() => (lastMinute() ? secondsText(secondsLeft()) : minutesText(minutesLeft())));
Pick one

Question 7 of 7

A payments ledger's account header shows a balance, a count of pending entries and how long ago it synced. The Angular version:

The React port:

function AccountHeader({ entries, secondsAgo }) {
  return (
    <>
      <Balance entries={entries} />
      <Pending entries={entries} />
      <Synced secondsAgo={secondsAgo} />
    </>
  );
}

One second passes: Angular sets secondsAgo to 1 and the template reads all three computeds once; React's parent renders AccountHeader again with secondsAgo 1 and the same entries.

Counting the child components of AccountHeader that render again and the Angular computeds that run their function, how many in all?

Code for this question
const entries = signal(loadLedger());const secondsAgo = signal(0);const balance = computed(() => sumBalance(entries()));const pending = computed(() => countPending(entries()));const synced = computed(() => syncedLabel(secondsAgo()));
Pick one

Go deeper

Pro tip
  1. Before porting a computed(), count its runs in React with a counter in the body. Put renders.current++ next to the derived value and type in an unrelated input: it rises every key, and that is fine for cheap work. When: deciding whether a port needs useMemo (step 8). Status: RAN (React 19.3.0, jsdom 30.1.1).
Sources

They support the teaching; no key is cited. Valid as of 2026-10-12 (Angular 22.2, React 19.3).

  • angular.dev, Signals: https://angular.dev/guide/signals (angular-signals.txt). Quoted: "Computed signals are both lazily evaluated and memoized" (steps 2–3) "The calculated value is then cached, and if you read doubleCount again, it will return the cached value without recalculating." (step 3) "Only the signals actually read during the derivation are tracked." (step 5) "Equality functions can be provided to both writable and computed signals." (step 4) "You can prevent a signal read from being tracked by calling its getter with untracked" (step 12)
  • angular.dev, Dependent state with linkedSignal: https://angular.dev/guide/signals/linked-signal (angular-linked-signal.txt), not named by the curriculum row; teaching support only. Quoted: "The linkedSignal updates its value when the source changes or when any signal referenced in the computation changes" (step 10)
  • react.dev, Choosing the State Structure: https://react.dev/learn/choosing-the-state-structure (react-choosing-state-structure.txt). Quoted: "If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state." (step 7)
  • react.dev, useMemo: https://react.dev/reference/react/useMemo (react-usememo.txt). Quoted: "If the overall logged time adds up to a significant amount (say, 1ms or more), it might make sense to memoize that calculation." (step 8) "React may throw away the cached value if there is a specific reason to do that" (step 9)

Checked against the tools and docs as of 2026-10-12.