skip to content

In Angular, how does linkedSignal()'s {source, computation} form use the previous value to keep a still-valid selection when its source changes?

level: middleimportance: should knowfreq 40%

answer

  1. split the trigger from the result
  2. computation gets the new source
  3. previous.source and previous.value
  4. undefined on the first run
  5. explicit generic types

basics

~10 s

With linkedSignal({source, computation}), the computation receives the new source value and a previous object holding the old source and the linked signal's current value. It can return previous.value when still valid instead of resetting.

solid answer

~40 s

The object form separates the trigger from the result: `source` is a signal (or function reading signals) whose change causes a recomputation, and `computation(newSource, previous)` produces the new value. `previous` is `{source, value}`: the source value used last time and the linked signal's current value, including one the user set. It is `undefined` on the first computation, so code must handle that. For shipping, `computation: (opts, prev) => opts.find(o => o.id === prev?.value.id) ?? opts[0]` keeps the user's choice when the new country still offers it and falls back otherwise. Signals read inside `computation` are tracked too, so the linked signal also recomputes when they change. When you use `previous`, the guide says to give `linkedSignal<S, D>` its type arguments explicitly.

code

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

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

declare function shippingOptionsFor(country: string): ShippingOption[];

@Component({
  selector: 'app-shipping-step',
  template: `<p>{{ selected().label }} ({{ selected().price }})</p>`,
})
export class ShippingStep {
  readonly country = signal('DE');
  readonly options = computed(() => shippingOptionsFor(this.country()));

  readonly selected = linkedSignal<ShippingOption[], ShippingOption>({
    source: this.options,
    computation: (options, previous) =>
      options.find(o => o.id === previous?.value.id) ?? options[0],
  });

  choose(option: ShippingOption) {
    this.selected.set(option);
  }
}

go deeper

for a junior

Recall that the object form takes a source and a computation, and that the computation can see the previous value to keep a choice.

for a middle

Explain what previous.source and previous.value hold, that previous is undefined at first, and that computation reads are tracked too.

for a senior

Show how you choose an identity strategy for preserving selections across server responses, and how you keep unwanted reads out of the computation.

for a principal

Decide how the team models selection state that must survive data refreshes, and where preservation rules belong: component, store or server.

## Why the shorthand is not always enough `linkedSignal(() => this.options()[0])` always resets to the first option when the options change. That is often wrong for users: if they chose **Express** in Germany and switch to Austria, where Express is also offered, resetting to the default loses a choice that is still valid. To decide *whether* to reset, the computation needs to know what the current value was. The **object form** provides that. ## The object form ```ts readonly selected = linkedSignal<ShippingOption[], ShippingOption>({ source: this.options, computation: (options, previous) => options.find(o => o.id === previous?.value.id) ?? options[0], }); ``` | Property | Type | Role | |---|---|---| | `source` | `() => S` | a signal or signal-reading function; its change triggers a recomputation | | `computation` | `(source: S, previous?: {source: S; value: D}) => D` | produces the new value | | `equal` | `(a: D, b: D) => boolean` | optional equality for the linked value | | `debugName` | `string` | optional label for DevTools | | `set` | `(value: D, rawSet) => void` | optional custom setter (22.1+) | ## What `previous` holds - `previous.source`: the **source value used in the last computation**, such as the previous options list. - `previous.value`: the linked signal's **current value** at the time of recomputation. If the user called `set()` since the last reset, this is the user's value, which is what makes "keep the user's choice" possible. - `previous` is **`undefined` on the first computation**, and also when the previous computation threw, so always use optional chaining or a guard. ## Walking through a country change 1. Initially `options` is the German list; `previous` is `undefined`, so the computation returns `options[0]`, DHL. 2. The user selects Express: `selected.set(express)`. 3. The user switches to Austria. `options` now returns the Austrian list, which also contains an option with id `express`. 4. On the next read, the computation runs with the Austrian list and `previous = {source: germanList, value: express}`. It finds `express` and returns the Austrian Express option. 5. The user switches to the US, where there is no Express. The computation finds nothing and returns the first US option. ## Rules to remember 1. **Both source and computation are tracked.** The guide states that the linked signal updates "when the source changes or when any signal referenced in the computation changes". If the computation reads another signal that should not trigger a reset, wrap that read in `untracked()`. 2. **`source` decides what "changed" means.** A source that is a `computed()` returning a fresh array on every upstream change will trigger recomputations even when the content is the same; the `previous`-based computation then decides whether that matters. 3. **Return the same object to keep the choice.** Returning `previous.value` itself (or a value that is equal under the linked signal's `equal`) keeps the current value; returning a different object replaces it. 4. **Give the generic types.** When the computation uses `previous`, the guide notes that the type arguments `linkedSignal<S, D>()` must be provided explicitly; the first is the source type, the second the value type. 5. **Keep the computation pure.** It runs lazily on read and must not write other signals; a write there throws `NG0600`. ## Comparing identity strategies | Strategy in the computation | Effect on the user's choice | |---|---| | `options[0]` | always reset to the default | | `options.find(o => o.id === previous?.value.id) ?? options[0]` | kept when an option with the same id exists, re-pointed to the new list's object | | `previous && options.includes(previous.value) ? previous.value : options[0]` | kept only if the exact same object is in the new list | The second strategy is usually right for data from a server, where each response creates new objects with stable ids.

  • In a linkedSignal() object form, what is previous on the very first computation?
    `undefined`. There is no earlier value yet, so the computation must handle the missing case, typically with optional chaining and a fallback such as the first option. `previous` is also `undefined` after a computation that threw, because there is no valid previous value.
  • In Angular, if the computation of a linkedSignal() reads a discount signal, what happens when the discount changes?
    The linked signal recomputes on its next read, because signals read inside the computation are tracked, just like the source. If that should not reset the selection, read the discount with `untracked()` or move it out of the computation.

saying these in an interview costs you the question

  • previous is always defined, so previous.value can be read directly.
  • previous.value is the last computed value, ignoring anything set() stored.
  • Only the source is tracked; signals read in the computation are ignored.
  • The object form stops the linked signal from being writable.
  • previous.source is the new source value passed to the computation.