linkedSignal (Angular 21)
What is linkedSignal?
Section intitulée « What is linkedSignal? »linkedSignal est une nouvelle primitive d’Angular 21 qui crée un signal dérivé d’un autre signal, mais vous permet de remplacer sa valeur. C’est parfait pour les formulaires où vous souhaitez dériver l’état initial d’une source mais permettre les modifications utilisateur.
Why Use It?
Section intitulée « Why Use It? »- ✅ Form fields - Initialize from data, allow edits
- ✅ Derived but editable - Compute initial value, let user change it
- ✅ Reset capability - Easily reset au derived value
- ✅ Cleaner than computed + signal - No manual synchronization
linkedSignal vs computed
Section intitulée « linkedSignal vs computed »| Feature | computed() |
linkedSignal() |
|---|---|---|
| Read-only | ✅ Yes | ❌ No (writable) |
| Derived from source | ✅ Yes | ✅ Yes |
| Can be set manually | ❌ No | ✅ Yes |
| Cas d’utilisation | Valeurs dérivées pures | Valeurs dérivées éditables |
Utilisation de base
Section intitulée « Utilisation de base »import { Component, signal, linkedSignal } from '@angular/core';
@Component({ selector: 'app-user-form', standalone: true, template: ` <div> <h3>Source: {{ sourceSignal() }}</h3> <h3>Linked: {{ linkedValue() }}</h3>
<button (click)="changeSource()">Change Source</button> <button (click)="changeLinked()">Change Linked</button> <button (click)="reset()">Reset Linked</button> </div> `})export class UserFormComponent { // Source signal sourceSignal = signal('Initial');
// Linked signal - derives from source but can be changed independently linkedValue = linkedSignal(() => this.sourceSignal());
changeSource() { this.sourceSignal.set('Source Changed'); // linkedValue se met à jour automatiquement ! }
changeLinked() { this.linkedValue.set('Manually Changed'); // Source stays the same }
reset() { // Réinitialiser à la valeur source actuelle this.linkedValue.set(this.sourceSignal()); }}Key Points:
linkedSignalstarts with value fromsourceSignal- Lorsque
sourceSignalchange,linkedValuese met à jour - You can manually set
linkedValueindependently - Great for forms that initialize from data
// Traditional approach (before linkedSignal)sourceData = signal({ name: 'John' });editableName = signal(this.sourceData().name);
// Problème : editableName ne se met pas à jour lorsque sourceData change// Vous devez les synchroniser manuellement
effect(() => { this.editableName.set(this.sourceData().name);});
// ❌ Verbose and error-prone// With linkedSignal (Angular 21)sourceData = signal({ name: 'John' });editableName = linkedSignal(() => this.sourceData().name);
// ✅ Se synchronise automatiquement avec la source, mais reste modifiable !Exemple de champ de formulaire
Section intitulée « Exemple de champ de formulaire »import { Component, signal, linkedSignal } from '@angular/core';
interface User { id: number; name: string; email: string;}
@Component({ selector: 'app-user-edit', standalone: true, template: ` <div> <h2>Edit User</h2>
<label> Name: <input [(ngModel)]="editableName" /> </label>
<label> Email: <input [(ngModel)]="editableEmail" /> </label>
<div> <button (click)="save()">Save</button> <button (click)="reset()">Reset</button> <button (click)="loadAnotherUser()">Load Another User</button> </div>
<div> <p>Original: {{ currentUser().name }} ({{ currentUser().email }})</p> <p>Editing: {{ editableName() }} ({{ editableEmail() }})</p> <p>Changed: {{ hasChanges() ? 'Yes' : 'No' }}</p> </div> </div> `})export class UserEditComponent { // Source data (e.g., from API) currentUser = signal<User>({ id: 1, name: 'John Doe', email: 'john@example.com' });
// Champs éditables liés à la source editableName = linkedSignal(() => this.currentUser().name); editableEmail = linkedSignal(() => this.currentUser().email);
// Vérifier si le formulaire a des changements hasChanges = computed(() => this.editableName() !== this.currentUser().name || this.editableEmail() !== this.currentUser().email );
save() { // Sauvegarder les changements dans la source this.currentUser.set({ ...this.currentUser(), name: this.editableName(), email: this.editableEmail() }); console.log('Saved!', this.currentUser()); }
reset() { // Réinitialiser le formulaire aux données utilisateur actuelles this.editableName.set(this.currentUser().name); this.editableEmail.set(this.currentUser().email); }
loadAnotherUser() { // Lorsque la source change, les signaux liés se mettent à jour automatiquement ! this.currentUser.set({ id: 2, name: 'Jane Smith', email: 'jane@example.com' }); // editableName et editableEmail se mettent à jour automatiquement ! }}Ce qui se passe :
- Les champs du formulaire s’initialisent depuis
currentUser - L’utilisateur peut modifier les champs
- Lorsque
currentUserchange (par ex., chargement d’un autre utilisateur), les champs du formulaire se mettent à jour automatiquement - Peut facilement se réinitialiser aux valeurs originales
// ❌ Traditional approach - manual synchronization needed@Component({ template: `...`})export class UserEditComponent { currentUser = signal<User>({ id: 1, name: 'John', email: 'john@example.com' });
// Separate signals for editing editableName = signal(''); editableEmail = signal('');
constructor() { // Synchronisation manuelle lorsque la source change effect(() => { this.editableName.set(this.currentUser().name); this.editableEmail.set(this.currentUser().email); }); }
// Problem: Effect runs on every change, even when user is editing // Can cause cursor jumps or lost edits!}// ✅ Avec linkedSignal - synchronisation automatique, pas d'effets secondaireseditableName = linkedSignal(() => this.currentUser().name);editableEmail = linkedSignal(() => this.currentUser().email);// Pas besoin d'effet ! Se synchronise automatiquement uniquement lorsque la source changeWith Computation
Section intitulée « With Computation »import { Component, signal, linkedSignal, computed } from '@angular/core';
@Component({ selector: 'app-price-editor', standalone: true, template: ` <div> <label> Base Price: ${{ basePrice() }} <input type="number" [(ngModel)]="basePrice" /> </label>
<label> Tax Rate: {{ taxRate() }}% <input type="number" [(ngModel)]="taxRate" /> </label>
<label> Final Price (editable): <input type="number" [(ngModel)]="editableFinalPrice" /> </label>
<button (click)="resetPrice()">Reset to Calculated</button>
<p>Calculated: ${{ calculatedPrice() }}</p> <p>Current: ${{ editableFinalPrice() }}</p> </div> `})export class PriceEditorComponent { basePrice = signal(100); taxRate = signal(10);
// Computed price calculatedPrice = computed(() => { const base = this.basePrice(); const tax = this.taxRate(); return base + (base * tax / 100); });
// Editable price that starts with calculated value editableFinalPrice = linkedSignal(() => this.calculatedPrice()); // L'utilisateur peut remplacer le prix, mais il se réinitialise lorsque les entrées changent
resetPrice() { this.editableFinalPrice.set(this.calculatedPrice()); }}Cas d’utilisation : Calculateur de prix où vous pouvez remplacer le prix final, mais il utilise par défaut la valeur calculée.
@Component({ selector: 'app-settings', standalone: true, template: ` <div> <h3>Theme Settings</h3>
<label> Theme: <select [(ngModel)]="editableTheme"> <option value="light">Light</option> <option value="dark">Dark</option> </select> </label>
<label> Font Size: <input type="number" [(ngModel)]="editableFontSize" /> </label>
<button (click)="save()">Save</button> <button (click)="reset()">Reset</button> <button (click)="loadDefaults()">Load Defaults</button> </div> `})export class SettingsComponent { // Settings from user preferences or defaults settings = signal({ theme: 'light', fontSize: 14, language: 'en' });
// Copies éditables liées à la source editableTheme = linkedSignal(() => this.settings().theme); editableFontSize = linkedSignal(() => this.settings().fontSize);
save() { this.settings.update(s => ({ ...s, theme: this.editableTheme(), fontSize: this.editableFontSize() })); }
reset() { this.editableTheme.set(this.settings().theme); this.editableFontSize.set(this.settings().fontSize); }
loadDefaults() { // Lorsque les paramètres changent, les signaux liés se mettent à jour automatiquement this.settings.set({ theme: 'dark', fontSize: 16, language: 'en' }); }}Real-World Pattern: Search with Override
Section intitulée « Real-World Pattern: Search with Override »import { Component, signal, linkedSignal } from '@angular/core';import { HttpClient } from '@angular/common/http';
@Component({ selector: 'app-user-search', standalone: true, template: ` <div> <input [(ngModel)]="searchTerm" placeholder="Search users..." (input)="onSearch()" />
<div *ngFor="let user of users()"> {{ user.name }} <button (click)="selectUser(user)">Select</button> </div>
<div *ngIf="selectedUser()"> <h3>Selected User</h3> <label> Name: <input [(ngModel)]="editableName" /> </label> <button (click)="resetName()">Reset to Original</button> </div> </div> `})export class UserSearchComponent { http = inject(HttpClient);
searchTerm = signal(''); users = signal<User[]>([]); selectedUser = signal<User | null>(null);
// Nom lié à l'utilisateur sélectionné editableName = linkedSignal(() => this.selectedUser()?.name ?? ''); // Se met à jour automatiquement lorsqu'un utilisateur différent est sélectionné !
onSearch() { this.http.get<User[]>(`/api/users?q=${this.searchTerm()}`) .subscribe(users => this.users.set(users)); }
selectUser(user: User) { this.selectedUser.set(user); // editableName se met à jour automatiquement au nom du nouvel utilisateur ! }
resetName() { this.editableName.set(this.selectedUser()?.name ?? ''); }}Pourquoi linkedSignal est parfait ici :
- Lorsque l’utilisateur sélectionne une personne différente, le champ du formulaire se met à jour automatiquement
- L’utilisateur peut toujours modifier le nom
- Facile à réinitialiser à la valeur originale
Bonnes Pratiques
Section intitulée « Bonnes Pratiques »- ✅ Utiliser pour les champs de formulaire dérivés de sources de données
- ✅ Parfait pour les formulaires d’édition qui chargent depuis une API
- ✅ Idéal pour les paramètres qui peuvent être remplacés
- ✅ Utiliser lorsque vous avez besoin d’une fonctionnalité de réinitialisation à la valeur par défaut
- ✅ Combiner avec
computed()pour les valeurs initiales dérivées - ❌ Ne pas utiliser si la valeur est purement calculée (utiliser
computed()) - ❌ Ne pas utiliser pour un état dérivé en lecture seule
- ❌ Éviter s’il n’y a pas de signal source auquel se lier
linkedSignal vs Alternatives
Section intitulée « linkedSignal vs Alternatives »| Approche | Cas d’utilisation |
|---|---|
| linkedSignal() | Derived from source, but can be edited |
| computed() | Pure derived value, read-only |
| signal() | Independent value, not derived |
| effect() | Effets secondaires, pas pour dériver des valeurs |
Common Use Cases
Section intitulée « Common Use Cases »- 📝 Edit forms - Load user data, allow edits, reset capability
- ⚙️ Settings panels - Default values with overrides
- 💰 Calculateurs de prix - Prix calculés qui peuvent être ajustés manuellement
- 🔍 Search & select - Select item, edit copy, switch selections
- 📊 Dashboard filters - Default filters derived from URL, can be changed
- 🎨 Éditeurs de thèmes - Dérivés des préférences utilisateur, personnalisables

