skip to content

In Angular, greeting reads firstName and a fullName computed built from firstName and lastName; why does greeting never see a half-updated name?

level: middleimportance: should knowfreq 40%

answer

  1. two paths to one node
  2. marks first, values later
  3. recursive version polling
  4. one recomputation per read
  5. effects see settled state

basics

~20 s

Angular never recomputes during a write: firstName.set() only marks fullName and greeting dirty. When greeting is read, it polls its producers and brings fullName up to date first, so it always runs once with the new firstName and the new fullName together.

solid answer

~40 s

This is a diamond: `firstName` reaches `greeting` directly and through `fullName`. In an eager system, `greeting` could rerun as soon as `firstName` notifies it, before `fullName` has updated, and briefly show the new first name next to the old full name — a glitch. Angular's push phase only marks nodes dirty, so nothing reruns mid-write. When `greeting` is read, it polls its producers by version; reading `fullName()` inside it recomputes `fullName` on demand, so every value `greeting` sees is current. `greeting` runs **once** per read, however many paths the change travelled and however many writes preceded it. Effects and templates run later, from Angular's scheduler, after your synchronous code has finished writing.

code

ts · 25 lines
ts
import { computed, signal } from '@angular/core';

const firstName = signal('Ada');
const lastName = signal('Lovelace');

let fullNameRuns = 0;
let greetingRuns = 0;

const fullName = computed(() => {
  fullNameRuns++;
  return `${firstName()} ${lastName()}`;
});

const greeting = computed(() => {
  greetingRuns++;
  return `Hi ${firstName()}, your badge reads ${fullName()}`;
});

greeting();              // fullNameRuns = 1, greetingRuns = 1

firstName.set('Grace');  // push: nothing recomputes yet
lastName.set('Hopper');  // push again: still nothing recomputes

greeting();              // pull: 'Hi Grace, your badge reads Grace Hopper'
                         // fullNameRuns = 2, greetingRuns = 2

go deeper

for a junior

Recall that a computed always returns a value consistent with the latest writes, even when its inputs are related.

for a middle

Explain the diamond, how dirty marking skips already-dirty nodes, and how reading fullName inside greeting brings it up to date first.

for a senior

Predict run counts for sequences of writes and reads, and distinguish a real glitch from a legitimate read taken between two writes.

for a principal

Explain why the team should derive through computed rather than sync signals by hand, and what guarantees they lose if they do not.

## The diamond A **diamond dependency** is a graph where one source reaches a node along two paths: - `firstName` and `lastName` are writable signals. - `fullName` is a `computed()` that joins the two names. - `greeting` is a `computed()` that reads `firstName` **and** `fullName`. ```ts const fullName = computed(() => `${firstName()} ${lastName()}`); const greeting = computed(() => `Hi ${firstName()}, your badge reads ${fullName()}`); ``` `firstName` feeds `greeting` directly and indirectly through `fullName`. When `firstName` changes, both of `greeting`'s inputs are affected, and the question is whether `greeting` can ever run with one input updated and the other stale. A value computed from such a mixture is called a **glitch**. This answer assumes Angular 22, though the mechanism has not changed since signals were introduced. ## How Angular avoids it The primitives package documents a **push/pull algorithm** with two strictly separated phases. 1. **Push (inside `set()`).** `firstName.set('Grace')` bumps the signal's version and marks its live consumers dirty: `fullName`, `greeting`, and any template or effect above them. `greeting` is reached twice — once directly and once through `fullName` — but a node that is already dirty is skipped, and **no computation runs**. Reading any signal during this phase is treated as an assertion error, so no user code can observe the graph half-marked. 2. **Pull (on read).** When `greeting()` is read — by a template refresh, an effect or plain code — it checks its producers in the order it read them last time. `firstName`'s version has changed, so `greeting` must rerun. Inside the rerun, the call to `fullName()` first brings `fullName` up to date: it polls `firstName` and `lastName`, recomputes, and returns `'Grace Lovelace'`. `greeting` therefore combines the new first name with the new full name. Because every read of a computed first makes that computed current, a computation can only ever see values that are consistent with the latest writes. ## How many times things run | Sequence | `fullName` runs | `greeting` runs | | --- | --- | --- | | First read of `greeting()` | 1 | 1 | | `firstName.set()`, then read | 1 more | 1 more | | `firstName.set()` and `lastName.set()`, then read | 1 more | 1 more | | Three writes, no read | 0 | 0 | The framework's own test suite asserts the "recompute only once for diamond dependency graph" behaviour. Several writes in a row do not multiply the work, because the push phase does no computation; the next pull recomputes once with the final values. ## What "glitch-free" does not mean - It does **not** mean a read between two writes shows both new values. If code reads `greeting()` after `firstName.set()` but before `lastName.set()`, it gets `'Hi Grace, your badge reads Grace Lovelace'` — correct for the state at that instant, not a glitch. - It does **not** make effects run synchronously. Effects are scheduled and run later, so an effect reading `greeting` normally sees the result of all the writes your event handler made. - It does **not** protect against writing signals from inside an effect to "sync" derived state; that creates a second round of updates and is a separate design problem. ## Why this matters in interviews Candidates often describe signals as "an event emitter that pushes new values". Under that model the diamond would glitch, and a component would render twice per change. Angular's model — push invalidation, pull values, compare versions — is what lets a template read ten computeds built on one signal and still refresh once with consistent data. ## Practical guidance - Derive with `computed()` rather than copying values between signals, so the graph can keep them consistent. - Do not add manual "wait until both are set" logic; the pull already sees the latest writes. - Use a counter inside a computation, as in the example, to check how often it runs.

  • What would an eager, push-values implementation do with this diamond?
    It would rerun `greeting` when `firstName` notifies it, possibly before `fullName` has recomputed, producing a greeting with the new first name and the old full name, then rerun it again once `fullName` updates. Angular avoids both the wrong intermediate value and the duplicate run by pushing only dirty marks.
  • If a template reads both fullName and greeting, how often does the view refresh after one write?
    Once. The write marks the component's single template consumer dirty; during the next change detection pass the view is refreshed once, and both reads pull already-consistent values.

saying these in an interview costs you the question

  • greeting reruns once for each path the change travels.
  • Signals push new values down the graph as soon as they are set.
  • You need to set both names inside a batch to avoid a glitch.
  • A read between two writes showing one new name is a glitch.
  • Effects run inside set(), so they can see a half-updated graph.