linkedSignal() is a writable signal whose value is recalculated automatically when its source changes. Think of it as a computed() that you can also set — until the next change of the source, when it “resets”. A real use: a page number that goes back to 1 when the filters change, on Lists.
| signal() | computed() | linkedSignal() | |
|---|---|---|---|
| Can be set | ✓ | ✕ | ✓ |
| Recalculated from other signals | ✕ | ✓ | ✓ |
| Typical example | filters, toggles | total, filtered list | selection, current page, draft |
The problem it solves
You have a list of options that depends on something else (the chosen country) and a selection that the user can change. When the list changes, the old selection may no longer be valid. Before linkedSignal, the solutions were poor:
// The problem: the selection depends on the list, but the user can also change it.
readonly options = computed(() => SHIPPING[this.country()]);
// ✗ computed can't be set (the user can't choose another option)
readonly selected = computed(() => this.options()[0]);
// ✗ signal + effect: works, but it's fragile (one render with a wrong value, one more run)
readonly selected = signal<ShippingOption | null>(null);
constructor() {
effect(() => this.selected.set(this.options()[0]));
}The short form and the long form
import { linkedSignal } from '@angular/core';
readonly options = computed(() => SHIPPING[this.country()]);
// ✓ Recalculated when `options` changes, BUT you can also set it by hand.
readonly selected = linkedSignal(() => this.options()[0]);
choose(option: ShippingOption) {
this.selected.set(option); // valid until the next change of options
}previous — keeps the selection if it also exists in the new country The “editable local copy of an input” pattern
input() is read-only in a component. When you want the user to edit a value received from the parent locally (an edit form, a draft), but to resynchronise when the parent sends another value, linkedSignal is exactly the right tool. It replaces the old ngOnChanges + local field combo.
@Component({
selector: 'app-name-editor',
template: `
<input [value]="draft()" (input)="draft.set($any($event.target).value)" />
@if (dirty()) { <span>changed</span> }
<button (click)="save.emit(draft())">Save</button>
`,
})
export class NameEditor {
readonly name = input.required<string>(); // read-only, comes from the parent
readonly save = output<string>();
// A local, editable copy that RESETS when the parent sends another name.
protected readonly draft = linkedSignal(() => this.name());
protected readonly dirty = computed(() => this.draft() !== this.name());
}The set option — intercepting writes v22
From v22.1 you can give a set function that receives the desired value and a rawSet. Useful for normalising or limiting values (clamp, trim, rounding) in one place, wherever the write comes from.
// New in v22.1: the `set` option intercepts any .set() / .update().
readonly stock = input.required<number>();
readonly quantity = linkedSignal(() => Math.min(1, this.stock()), {
set: (value, rawSet) => {
const clamped = Math.min(Math.max(Math.round(value) || 1, 1), this.stock());
rawSet(clamped); // the “real” write
},
});
this.quantity.set(99); // → the value becomes stock()
this.quantity.set(-3); // → 1set limits to 1…5Another classic example: pagination
// Classic example: the page goes back to 1 when the filters change
readonly filters = signal<Filters>({ query: '', category: null });
readonly page = linkedSignal({
source: this.filters,
computation: () => 1,
});
nextPage() {
this.page.update((p) => p + 1); // the user navigates…
}
// …and on any change of filters, page() goes back to 1, automatically.When YES, when NO
- A selection that depends on a list (dependent dropdown, active tab, selected row).
- The current page that goes back to 1 when the filters change.
- An editable copy of an
input()(draft, edit form). - A value that is “the default until the user picks something else”.
- A purely derived value that nobody sets →
computed(). - Independent state →
signal(). - Server data →
resource()(which also has.set()for optimistic updates). - Two-way binding with the parent →
model().
Common pitfalls
- “Losing” changes: any change of the source overwrites what the user set. If you don’t want that, use the long form with
previous. - Source too broad: if the source is a large object that changes often, the selection resets often. Make the source as precise as possible (a
computedwith only what matters) or give it anequal. - Types in the long form: if TypeScript doesn’t infer well, write
linkedSignal<Source, Value>(...)explicitly.
Cheat sheet
// short: derived + writable
const sel = linkedSignal(() => list()[0]);
// long: a separate source + the previous value
const sel2 = linkedSignal<Item[], Item>({
source: list,
computation: (items, prev) => items.find((i) => i.id === prev?.value.id) ?? items[0],
equal: (a, b) => a.id === b.id,
});
// v22.1: intercepting writes
const qty = linkedSignal(() => 1, { set: (v, rawSet) => rawSet(Math.max(1, v)) });
sel.set(x); sel.update((v) => ...); sel(); // exactly like a signal()