linkedSignal (Angular 21)
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?
- ✅ 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
| 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
- Basic Example
- How It Works
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
- Edit Form
- Without linkedSignal
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 secondaires
editableName = linkedSignal(() => this.currentUser().name);
editableEmail = linkedSignal(() => this.currentUser().email);
// Pas besoin d'effet ! Se synchronise automatiquement uniquement lorsque la source change
With Computation
- Computed Initial Value
- Object Property
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
- Search Component
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
- ✅ 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
| 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
- 📝 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