linkedSignal() ist ein beschreibbares Signal, dessen Wert automatisch neu berechnet wird, wenn sich seine Quelle ändert. Stell es dir als ein computed() vor, das du auch setzen kannst — bis zur nächsten Änderung der Quelle, dann „setzt“ es sich „zurück“. Ein echter Einsatz: eine Seitenzahl, die auf 1 zurückgeht, wenn sich die Filter ändern — unter Listen.
| signal() | computed() | linkedSignal() | |
|---|---|---|---|
| Kann gesetzt werden | ✓ | ✕ | ✓ |
| Wird aus anderen Signals neu berechnet | ✕ | ✓ | ✓ |
| Typisches Beispiel | Filter, Toggles | Summe, gefilterte Liste | Auswahl, aktuelle Seite, Entwurf |
Das Problem, das es löst
Du hast eine Liste von Optionen, die von etwas anderem abhängt (dem gewählten Land), und eine Auswahl, die der Nutzer ändern kann. Ändert sich die Liste, ist die alte Auswahl womöglich nicht mehr gültig. Vor linkedSignal waren die Lösungen schwach:
// Das Problem: Die Auswahl hängt von der Liste ab, aber der Nutzer kann sie auch ändern.
readonly options = computed(() => SHIPPING[this.country()]);
// ✗ computed lässt sich nicht setzen (der Nutzer kann keine andere Option wählen)
readonly selected = computed(() => this.options()[0]);
// ✗ signal + effect: funktioniert, ist aber fragil (ein Rendering mit falschem Wert, ein zusätzlicher Durchlauf)
readonly selected = signal<ShippingOption | null>(null);
constructor() {
effect(() => this.selected.set(this.options()[0]));
}Die kurze und die lange Form
import { linkedSignal } from '@angular/core';
readonly options = computed(() => SHIPPING[this.country()]);
// ✓ Wird neu berechnet, wenn sich `options` ändert, du kannst es ABER auch von Hand setzen.
readonly selected = linkedSignal(() => this.options()[0]);
choose(option: ShippingOption) {
this.selected.set(option); // gültig bis zur nächsten Änderung von options
}previous — behält die Auswahl, wenn sie auch im neuen Land existiert Das Muster „bearbeitbare lokale Kopie eines Inputs“
input() ist in einer Komponente schreibgeschützt. Wenn der Nutzer einen vom Parent erhaltenen Wert lokal bearbeiten können soll (Bearbeitungsformular, Entwurf), sich aber neu synchronisieren soll, sobald der Parent einen anderen Wert sendet, ist linkedSignal genau das richtige Werkzeug. Es ersetzt die alte Kombination aus ngOnChanges und lokalem Feld.
@Component({
selector: 'app-name-editor',
template: `
<input [value]="draft()" (input)="draft.set($any($event.target).value)" />
@if (dirty()) { <span>geändert</span> }
<button (click)="save.emit(draft())">Speichern</button>
`,
})
export class NameEditor {
readonly name = input.required<string>(); // schreibgeschützt, kommt vom Parent
readonly save = output<string>();
// Eine lokale, bearbeitbare Kopie, die sich ZURÜCKSETZT, wenn der Parent einen anderen Namen sendet.
protected readonly draft = linkedSignal(() => this.name());
protected readonly dirty = computed(() => this.draft() !== this.name());
}Die Option set — Schreibzugriffe abfangen v22
Ab v22.1 kannst du eine set-Funktion angeben, die den gewünschten Wert und ein rawSet erhält. Praktisch, um Werte an einer einzigen Stelle zu normalisieren oder zu begrenzen (Clamp, Trim, Runden) — egal, woher der Schreibzugriff kommt.
// Neu in v22.1: Die Option `set` fängt jedes .set() / .update() ab.
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); // der „echte“ Schreibzugriff
},
});
this.quantity.set(99); // → der Wert wird zu stock()
this.quantity.set(-3); // → 1set begrenzt auf 1…5Ein weiteres Klassiker-Beispiel: Paginierung
// Klassisches Beispiel: Die Seite springt auf 1 zurück, wenn sich die Filter ändern
readonly filters = signal<Filters>({ query: '', category: null });
readonly page = linkedSignal({
source: this.filters,
computation: () => 1,
});
nextPage() {
this.page.update((p) => p + 1); // der Nutzer blättert …
}
// … und bei jeder Änderung der Filter springt page() automatisch wieder auf 1.Wann ja, wann nein
- Eine Auswahl, die von einer Liste abhängt (abhängiges Dropdown, aktiver Tab, gewählte Zeile).
- Die aktuelle Seite, die auf 1 zurückspringt, wenn sich die Filter ändern.
- Eine bearbeitbare Kopie eines
input()(Entwurf, Bearbeitungsformular). - Ein Wert, der „der Standard ist, bis der Nutzer etwas anderes wählt“.
- Ein rein abgeleiteter Wert, den niemand setzt →
computed(). - Unabhängiger Zustand →
signal(). - Serverdaten →
resource()(das für optimistische Updates ebenfalls.set()hat). - Two-Way-Binding mit dem Parent →
model().
Häufige Stolperfallen
- Änderungen „gehen verloren“: Jede Änderung der Quelle überschreibt, was der Nutzer gesetzt hat. Willst du das nicht, nutze die lange Form mit
previous. - Zu breite Quelle: Ist die Quelle ein großes Objekt, das sich oft ändert, setzt sich die Auswahl oft zurück. Mach die Quelle so präzise wie möglich (ein
computedmit nur dem, was zählt) oder gib ihr einequal. - Typen in der langen Form: Wenn TypeScript sie nicht gut inferiert, schreib
linkedSignal<Source, Value>(...)explizit hin.
Spickzettel
// kurz: abgeleitet + beschreibbar
const sel = linkedSignal(() => list()[0]);
// lang: separate Quelle + vorheriger Wert
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: Schreibzugriffe abfangen
const qty = linkedSignal(() => 1, { set: (v, rawSet) => rawSet(Math.max(1, v)) });
sel.set(x); sel.update((v) => ...); sel(); // genau wie ein signal()