linkedSignal (Angular 21)
What is linkedSignal?
Section titled “What is linkedSignal?”linkedSignal is a new Angular 21 primitive that creates a signal derived from another signal, but allows you to override its value. It’s perfect for forms where you want to derive initial state from a source but allow user edits.
Why Use It?
Section titled “Why Use It?”- ✅ Form fields - Initialize from data, allow edits
- ✅ Derived but editable - Compute initial value, let user change it
- ✅ Reset capability - Easily reset to derived value
- ✅ Cleaner than computed + signal - No manual synchronization
linkedSignal vs computed
Section titled “linkedSignal vs computed”| Feature | computed() |
linkedSignal() |
|---|---|---|
| Read-only | ✅ Yes | ❌ No (writable) |
| Derived from source | ✅ Yes | ✅ Yes |
| Can be set manually | ❌ No | ✅ Yes |
| Use case | Pure derived values | Editable derived values |
Basic Usage
Section titled “Basic Usage”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 automatically updates! }
changeLinked() { this.linkedValue.set('Manually Changed'); // Source stays the same }
reset() { // Reset to current source value this.linkedValue.set(this.sourceSignal()); }}Key Points:
linkedSignalstarts with value fromsourceSignal- When
sourceSignalchanges,linkedValueupdates - 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);
// Problem: editableName doesn't update when sourceData changes// You need to manually sync them
effect(() => { this.editableName.set(this.sourceData().name);});
// ❌ Verbose and error-prone// With linkedSignal (Angular 21)sourceData = signal({ name: 'John' });editableName = linkedSignal(() => this.sourceData().name);
// ✅ Auto-syncs with source, but still writable!Form Field Example
Section titled “Form Field Example”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' });
// Editable fields linked to source editableName = linkedSignal(() => this.currentUser().name); editableEmail = linkedSignal(() => this.currentUser().email);
// Check if form has changes hasChanges = computed(() => this.editableName() !== this.currentUser().name || this.editableEmail() !== this.currentUser().email );
save() { // Save changes back to source this.currentUser.set({ ...this.currentUser(), name: this.editableName(), email: this.editableEmail() }); console.log('Saved!', this.currentUser()); }
reset() { // Reset form to current user data this.editableName.set(this.currentUser().name); this.editableEmail.set(this.currentUser().email); }
loadAnotherUser() { // When source changes, linked signals auto-update! this.currentUser.set({ id: 2, name: 'Jane Smith', email: 'jane@example.com' }); // editableName and editableEmail automatically update! }}What happens:
- Form fields initialize from
currentUser - User can edit the fields
- When
currentUserchanges (e.g., loading another user), form fields automatically update - Can easily reset to original values
// ❌ 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() { // Manually sync when source changes 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!}// ✅ With linkedSignal - automatic sync, no side effectseditableName = linkedSignal(() => this.currentUser().name);editableEmail = linkedSignal(() => this.currentUser().email);// No effect needed! Auto-syncs only when source changesWith Computation
Section titled “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()); // User can override the price, but it resets when inputs change
resetPrice() { this.editableFinalPrice.set(this.calculatedPrice()); }}Use case: Price calculator where you can override the final price, but it defaults to calculated value.
@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' });
// Editable copies linked to 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() { // When settings change, linked signals auto-update this.settings.set({ theme: 'dark', fontSize: 16, language: 'en' }); }}Real-World Pattern: Search with Override
Section titled “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);
// Name linked to selected user editableName = linkedSignal(() => this.selectedUser()?.name ?? ''); // Auto-updates when different user is selected!
onSearch() { this.http.get<User[]>(`/api/users?q=${this.searchTerm()}`) .subscribe(users => this.users.set(users)); }
selectUser(user: User) { this.selectedUser.set(user); // editableName automatically updates to new user's name! }
resetName() { this.editableName.set(this.selectedUser()?.name ?? ''); }}Why linkedSignal is perfect here:
- When user selects different person, form field auto-updates
- User can still edit the name
- Easy to reset to original value
Best Practices
Section titled “Best Practices”- ✅ Use for form fields that derive from data sources
- ✅ Perfect for edit forms that load from API
- ✅ Great for settings that can be overridden
- ✅ Use when you need reset to default functionality
- ✅ Combine with
computed()for derived initial values - ❌ Don’t use if value is purely computed (use
computed()) - ❌ Don’t use for read-only derived state
- ❌ Avoid if no source signal to link to
linkedSignal vs Alternatives
Section titled “linkedSignal vs Alternatives”| Approach | Use Case |
|---|---|
| linkedSignal() | Derived from source, but can be edited |
| computed() | Pure derived value, read-only |
| signal() | Independent value, not derived |
| effect() | Side effects, not for deriving values |
Common Use Cases
Section titled “Common Use Cases”- 📝 Edit forms - Load user data, allow edits, reset capability
- ⚙️ Settings panels - Default values with overrides
- 💰 Price calculators - Computed prices that can be manually adjusted
- 🔍 Search & select - Select item, edit copy, switch selections
- 📊 Dashboard filters - Default filters derived from URL, can be changed
- 🎨 Theme editors - Derived from user preferences, customizable

