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?
const now = signal(0); // seconds since the sitting started; a timer sets itconst remaining = computed(() => formatLeft(5400 - now()));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
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?
const now = signal(0); // seconds since the hold startedconst filter = signal(''); // the seat filter boxconst left = computed(() => formatLeft(600 - now()));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?
const amountCents = signal(4500);const isLarge = computed(() => amountCents() > 100000);const badge = computed(() => badgeFor(isLarge()));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?
const channels = signal(['general', 'random', 'help']);const selected = linkedSignal(() => channels()[0]);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?
const queue = signal(loadQueue()); // 2,000 episodesconst position = signal(0); // seconds into the current episodeconst total = computed(() => sumDurations(queue()));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?
const lastMinute = signal(false);const minutesLeft = signal(2);const secondsLeft = signal(120);const label = computed(() => (lastMinute() ? secondsText(secondsLeft()) : minutesText(minutesLeft())));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?
const entries = signal(loadLedger());const secondsAgo = signal(0);const balance = computed(() => sumBalance(entries()));const pending = computed(() => countPending(entries()));const synced = computed(() => syncedLabel(secondsAgo()));Go deeper
Pro tip
- Before porting a
computed(), count its runs in React with a counter in the body. Putrenders.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 needsuseMemo(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)
Related lessons
- Read first: useState: hook slots, snapshots and batching
- Related: Render and commit
- Related: Memoization and React Compiler
- Related: State ownership taxonomy
- Related: Reconciliation, identity and keys
- Related: Change detection and OnPush vs re-rendering and memo
- Related: Dependency injection vs context and modules
Checked against the tools and docs as of 2026-10-12.