skip to content

In Angular, what does linkedSignal() give you that neither a plain signal() nor a computed() does?

level: juniorimportance: must knowfreq 50%

answer

  1. writable, yet derived
  2. resets when its source changes
  3. a computation instead of an initial value
  4. the user's choice survives until then

basics

~20 s

linkedSignal() is a writable signal whose value is initialised and reset by a computation. You can set() it like a signal(), but whenever the signals its computation reads change, it recomputes and replaces whatever was set.

solid answer

~40 s

A `signal()` is writable but knows nothing about other state: a selected shipping option stays selected even after the country changes and that option no longer exists. A `computed()` follows its inputs but is read-only, so the user cannot pick anything. `linkedSignal()` from `@angular/core` combines the two. Instead of an initial value you pass a computation, for example `linkedSignal(() => this.options()[0])`. It returns a `WritableSignal`, so `set()` and `update()` work and the user's choice is held; but when a signal the computation reads changes, the next read recomputes and the manual value is replaced. It is lazy, like `computed()`. linkedSignal arrived in v19 as a developer preview and has been stable since v20.

code

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

interface ShippingOption {
  id: string;
  label: string;
  price: number;
}

const OPTIONS: Record<'DE' | 'US', ShippingOption[]> = {
  DE: [{id: 'dhl', label: 'DHL', price: 5}, {id: 'express', label: 'Express', price: 15}],
  US: [{id: 'usps', label: 'USPS', price: 7}, {id: 'ups', label: 'UPS', price: 12}],
};

@Component({
  selector: 'app-shipping-picker',
  template: `
    <select (change)="country.set($any($event.target).value)">
      <option value="DE">Germany</option>
      <option value="US">United States</option>
    </select>
    @for (o of options(); track o.id) {
      <label>
        <input type="radio" [checked]="o.id === selected().id" (change)="selected.set(o)" />
        {{ o.label }}
      </label>
    }
  `,
})
export class ShippingPicker {
  readonly country = signal<'DE' | 'US'>('DE');
  readonly options = computed(() => OPTIONS[this.country()]);
  readonly selected = linkedSignal(() => this.options()[0]);
}

go deeper

for a junior

Recall that linkedSignal() is a writable signal whose default comes from a computation and resets when that computation's signals change, for example a selection that resets when its list changes.

for a middle

Explain how it differs from signal() and computed(), that it is lazy, and that set() stores a local value until the next reset.

for a senior

Show where linkedSignal replaces reset effects in real screens, and when computed() is still the clearer choice.

for a principal

Frame linkedSignal as a design tool for local-versus-derived state and decide how the team documents which values may be overridden.

## The problem it solves A checkout page lets the user choose a **country** and then a **shipping option** from the options available there. The selection is local, writable UI state, but it is only valid relative to the current list of options. With the two classic primitives, both halves fail: | Primitive | Can the user pick? | Follows the country? | Outcome | |---|---|---|---| | `signal(options()[0])` | yes | no | after a country change it still holds an option that no longer exists | | `computed(() => options()[0])` | no | yes | always the first option; the user's choice cannot be stored | | `linkedSignal(() => options()[0])` | yes | yes | the user's choice holds until the options change, then resets | ## What `linkedSignal()` is `linkedSignal()` from `@angular/core` creates a **writable signal whose value is initialised and reset by a reactive computation**: ```ts readonly country = signal<'DE' | 'US'>('DE'); readonly options = computed(() => shippingOptionsFor(this.country())); readonly selected = linkedSignal(() => this.options()[0]); ``` - It returns a **`WritableSignal`**, so `selected.set(option)` and `selected.update(fn)` work, and `asReadonly()` is available. - Its value starts as the computation's result. - The computation is **reactive**: every signal it reads is a dependency, exactly as in `computed()`. - When a dependency changes, the next read **recomputes** and the result **replaces** the current value, including a value the user set. - It is **lazy**: nothing is recomputed until someone reads it. This form, a single function, is the **shorthand**. There is also an object form with separate `source` and `computation` properties, which gives the computation access to the previous source value and the previous linked value, for cases such as "keep the user's choice if it is still available". ## Everyday use 1. Read it like any signal: `selected()` in code, `{{ selected()?.label }}` in a template. 2. Write it from the event handler where the user chooses: `selected.set(option)`. 3. Change the upstream state normally: `country.set('US')`. The next read of `selected()` yields the first US option. No effect, no subscription, no manual reset code. ## Options it accepts - `equal`: a custom equality function for its value, as with other signals. - `debugName`: a label for Angular DevTools. - `set` (added in **22.1**): a custom setter receiving the new value and a `rawSet` function. It lets `set()`/`update()` write back to a source of truth, for example setting a nested field on a parent object signal, instead of storing the value locally. ## What it is not - It is not a **two-way binding** to its source. A plain `set()` changes the linked signal only; `options` and `country` are untouched unless you use the custom `set` option. - It is not an **effect**. There is no scheduled run; the reset happens as part of reading, so no reader ever sees the new country together with a stale option. - It is not a replacement for **`computed()`**. If nobody ever needs to override the value, a `computed()` states the intent more clearly and cannot be written by mistake. - It cannot be written **inside a computation**: writing to it from a `computed()` or a template throws `NG0600`, like any signal. ## When interviewers bring it up The typical prompts are "how do you reset a selection when its list changes?", "why not an effect?", and "what does `linkedSignal` add over `computed`?". A strong answer names the three properties together: **writable**, **derived by default**, and **reset when its dependencies change**, plus the fact that it is stable API since v20.

  • In Angular, does calling set() on a linkedSignal() change the signals its computation reads?
    No. By default `set()` and `update()` store a value in the linked signal only; the source signals are untouched, and the next change to them will reset the value. Since 22.1 you can pass a custom `set` option, which receives the value and a `rawSet` function, to write back to a source of truth instead.
  • In Angular, when would you choose computed() over linkedSignal() even though both follow the same inputs?
    Whenever nothing should ever override the value. `computed()` is read-only, so the type prevents accidental writes and tells readers the value is purely derived. `linkedSignal()` is for local, user-editable state that should fall back to a derived default when its inputs change.

A linkedSignal is like a restaurant's daily special pre-selected on your order pad: you can cross it out and choose something else, but when the kitchen changes the menu, the pad is reset to the new special.

saying these in an interview costs you the question

  • linkedSignal() is read-only, like computed().
  • set() on a linkedSignal also updates the signals it depends on.
  • A manually set value survives any later change to the source.
  • linkedSignal() needs an effect() to perform the reset.
  • linkedSignal() takes an initial value, not a computation.