linkedSignal (Angular 21)
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?
- ✅ 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
| 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
- 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 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
- 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'
});
// 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 effects
editableName = linkedSignal(() => this.currentUser().name);
editableEmail = linkedSignal(() => this.currentUser().email);
// No effect needed! Auto-syncs only when source changes
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());
// 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
- 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);
// 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
- ✅ 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
| 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
- 📝 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