linkedSignal() est un signal modifiable dont la valeur est recalculée automatiquement quand sa source change. Voyez-le comme un computed() que vous pouvez aussi définir, jusqu’au prochain changement de la source, où il se « réinitialise ». Un vrai cas d’usage : un numéro de page qui revient à 1 quand les filtres changent, dans Listes.
| signal() | computed() | linkedSignal() | |
|---|---|---|---|
| Modifiable | ✓ | ✕ | ✓ |
| Recalculé à partir d’autres signals | ✕ | ✓ | ✓ |
| Exemple typique | filtres, bascules | total, liste filtrée | sélection, page courante, brouillon |
Le problème résolu
Vous avez une liste d’options qui dépend d’autre chose (le pays choisi) et une sélection que l’utilisateur peut modifier. Quand la liste change, l’ancienne sélection n’est peut-être plus valide. Avant linkedSignal, les solutions étaient médiocres :
// Le problème : la sélection dépend de la liste, mais l’utilisateur peut aussi la modifier.
readonly options = computed(() => SHIPPING[this.country()]);
// ✗ un computed ne peut pas être défini (l’utilisateur ne peut pas choisir une autre option)
readonly selected = computed(() => this.options()[0]);
// ✗ signal + effect : ça marche, mais c’est fragile (un rendu avec une mauvaise valeur, une exécution de plus)
readonly selected = signal<ShippingOption | null>(null);
constructor() {
effect(() => this.selected.set(this.options()[0]));
}La forme courte et la forme longue
import { linkedSignal } from '@angular/core';
readonly options = computed(() => SHIPPING[this.country()]);
// ✓ Recalculé quand `options` change, MAIS vous pouvez aussi le définir à la main.
readonly selected = linkedSignal(() => this.options()[0]);
choose(option: ShippingOption) {
this.selected.set(option); // valable jusqu’au prochain changement de options
}previous — conserve la sélection si elle existe aussi dans le nouveau pays Le pattern « copie locale modifiable d’un input »
input() est en lecture seule dans un composant. Quand vous voulez que l’utilisateur modifie localement une valeur reçue du parent (formulaire d’édition, brouillon), mais que celle-ci se resynchronise quand le parent envoie une autre valeur, linkedSignal est exactement le bon outil. Il remplace l’ancien combo ngOnChanges + champ local.
@Component({
selector: 'app-name-editor',
template: `
<input [value]="draft()" (input)="draft.set($any($event.target).value)" />
@if (dirty()) { <span>modifié</span> }
<button (click)="save.emit(draft())">Enregistrer</button>
`,
})
export class NameEditor {
readonly name = input.required<string>(); // en lecture seule, vient du parent
readonly save = output<string>();
// Une copie locale modifiable qui se RÉINITIALISE quand le parent envoie un autre nom.
protected readonly draft = linkedSignal(() => this.name());
protected readonly dirty = computed(() => this.draft() !== this.name());
}L’option set : intercepter les écritures v22
À partir de la v22.1, vous pouvez fournir une fonction set qui reçoit la valeur souhaitée et un rawSet. Utile pour normaliser ou borner les valeurs (clamp, trim, arrondi) à un seul endroit, quelle que soit l’origine de l’écriture.
// Nouveau en v22.1 : l’option `set` intercepte tout .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); // l’écriture « réelle »
},
});
this.quantity.set(99); // → la valeur devient stock()
this.quantity.set(-3); // → 1set personnalisé limite à 1…5Un autre exemple classique : la pagination
// Exemple classique : la page revient à 1 quand les filtres changent
readonly filters = signal<Filters>({ query: '', category: null });
readonly page = linkedSignal({
source: this.filters,
computation: () => 1,
});
nextPage() {
this.page.update((p) => p + 1); // l’utilisateur navigue…
}
// …et à chaque changement de filtres, page() revient automatiquement à 1.Quand OUI, quand NON
- Une sélection qui dépend d’une liste (dropdown dépendant, onglet actif, ligne sélectionnée).
- La page courante qui revient à 1 quand les filtres changent.
- Une copie modifiable d’un
input()(brouillon, formulaire d’édition). - Une valeur qui est « la valeur par défaut tant que l’utilisateur n’a pas choisi autre chose ».
- Une valeur purement dérivée que personne ne définit →
computed(). - Un état indépendant →
signal(). - Des données serveur →
resource()(qui a aussi.set()pour les mises à jour optimistes). - Un Binding bidirectionnel avec le parent →
model().
Pièges fréquents
- « Perdre » des modifications : tout changement de la source écrase ce que l’utilisateur a défini. Si vous ne le voulez pas, utilisez la forme longue avec
previous. - Source trop large : si la source est un gros objet qui change souvent, la sélection se réinitialise souvent. Rendez la source aussi précise que possible (un
computedavec seulement ce qui compte) ou donnez-lui unequal. - Types dans la forme longue : si TypeScript n’infère pas bien, écrivez explicitement
linkedSignal<Source, Value>(...).
Aide-mémoire
// court : dérivé + modifiable
const sel = linkedSignal(() => list()[0]);
// long : une source séparée + la valeur précédente
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 : intercepter les écritures
const qty = linkedSignal(() => 1, { set: (v, rawSet) => rawSet(Math.max(1, v)) });
sel.set(x); sel.update((v) => ...); sel(); // exactement comme un signal()