Angular linkedSignal: Writable State That Resets When Its Source Changes [2026]
computed() gives you derived state that is read-only. signal() gives you writable state with no dependency on anything else. What about state that needs to be BOTH derived from a source AND independently writable — a form input that defaults to a user's saved preference but the user can override; a selected-tab index that resets when the tab list changes? That's what linkedSignal() is for. Added in v19, it closes the last gap in the signal API.
This is lesson 3.8, the closing lesson of Module 3. After this, every signal primitive Angular ships is in your toolkit.
The problem linkedSignal() solves #
Consider a tab component where the user picks the active tab:
export class TabsComponent {
tabs = input.required<Tab[]>();
activeIdx = signal(0); // user's selection
}
Bug: when the parent passes a new tabs array (e.g., filtered to fewer tabs), activeIdx stays at whatever the user clicked. If they had selected tab 4 and now there are only 3 tabs, the active index is out of range.
Fix attempt with effect():
export class TabsComponent {
tabs = input.required<Tab[]>();
activeIdx = signal(0);
constructor() {
effect(() => {
this.tabs(); // tracked dependency
this.activeIdx.set(0); // reset on every change
}, { allowSignalWrites: true });
}
}
Works but awkward — requires allowSignalWrites, easy to forget, runs every time tabs changes regardless of whether the index is still valid.
linkedSignal() is the dedicated primitive for this:
import { linkedSignal, input } from '@angular/core';
export class TabsComponent {
tabs = input.required<Tab[]>();
// Writable, BUT auto-resets to 0 whenever tabs changes
activeIdx = linkedSignal(() => 0, { source: this.tabs });
selectTab(idx: number) { this.activeIdx.set(idx); } // user can still override
}
When tabs() changes, activeIdx resets to 0. Until then, the user's .set(idx) calls take effect normally. Best of both worlds.
The signature #
The simple form:
function linkedSignal<T>(
computation: () => T,
options?: { equal?: ValueEqualityFn<T> }
): WritableSignal<T>
The "source + computation" form (most useful):
function linkedSignal<S, D>(options: {
source: () => S | Signal<S>;
computation: (source: S, previous?: { source: S; value: D }) => D;
equal?: ValueEqualityFn<D>;
}): WritableSignal<D>
Three facts:
- The computation runs initially AND whenever
sourcechanges — same ascomputed()for that aspect - The result is a WRITABLE signal — you can
.set()and.update()to override - The next source change resets to the computation — overrides are temporary, not sticky
A second pattern: smart reset based on previous state #
The previous argument to computation lets you preserve state across source changes when it still makes sense:
activeTab = linkedSignal({
source: () => this.tabs(),
computation: (tabs, previous) => {
// If the previously-active tab still exists, keep it
if (previous && tabs.some(t => t.id === previous.value)) {
return previous.value;
}
// Otherwise reset to the first tab
return tabs[0]?.id;
},
});
Now the user's selection survives source changes WHEN POSSIBLE. The tab list reordering keeps your selection; only a tab being deleted bumps you to the first one.
This is the pattern that distinguishes linkedSignal() from a naive "reset on source change" — it can intelligently preserve user intent.
When to reach for linkedSignal() #
Four real-world patterns:
1. Form input with a smart default #
userPrefs = inject(UserService).preferences;
// Defaults to the user's saved value, but user can override mid-session
fontSize = linkedSignal(() => this.userPrefs().fontSize);
// Template: <input type="number" [value]="fontSize()" (input)="fontSize.set(+$any($event.target).value)" />
When the user's saved preferences change (logged into a new account, for example), fontSize updates. While they're in the session, they can adjust it freely.
2. Selected item in a dynamic list #
The canonical case shown above — the active index resets when the underlying list changes.
3. Computed default that can be edited #
firstName = signal('Pradeep');
lastName = signal('Bose');
// Default display name, editable
displayName = linkedSignal(() => `${this.firstName()} ${this.lastName()}`);
// Template lets user edit displayName freely. When they edit first/last, displayName resets
Useful for fields like "display name" or "slug" that derive from other fields but the user can manually override.
4. Pagination state #
items = input.required<Item[]>();
pageSize = signal(20);
currentPage = linkedSignal({
source: () => ({ items: this.items(), pageSize: this.pageSize() }),
computation: () => 1, // reset to page 1 when items or pageSize changes
});
When the underlying list or page size changes (filter applied, results re-fetched), pagination resets to page 1.
linkedSignal vs computed vs signal #
| Need | Primitive |
|---|---|
| Read-only value derived from signals | computed() |
| Writable value, no dependency on anything | signal() |
| Writable value that DERIVES from a source and RESETS when the source changes | linkedSignal() |
| Two-way binding between parent and child | model() |
| Async-backed value with cancellation | resource() |
A cheat-sheet question: "can the user override this value?" If yes, NOT computed(). If yes AND it should reset on source change, linkedSignal(). If yes AND it should never auto-reset, plain signal().
Equality control #
Like signal() and computed(), linkedSignal() accepts a custom equality function. Useful when the computation returns a new object reference each time:
config = linkedSignal({
source: () => this.theme(),
computation: (theme) => ({ ...defaultConfig, ...theme.overrides }),
equal: (a, b) => JSON.stringify(a) === JSON.stringify(b), // value-based equality
});
Otherwise downstream consumers would re-run on every theme change even when the merged config is identical.
Edge cases #
User write before first source emission #
The computation runs eagerly on first read of the linked signal — same as computed(). If you .set() before reading, the computation runs once on next read and your value is preserved until the next source change.
Source that is a computed() chain #
The source option accepts any signal — a writable signal, a computed, an input signal. The computation re-fires whenever any of source's transitive dependencies change:
filteredTabs = computed(() => this.tabs().filter(t => t.visible));
activeIdx = linkedSignal(() => 0, { source: filteredTabs });
When tabs() changes, filteredTabs() re-runs, then activeIdx resets. All transitive.
Order of operations #
If both the source signal AND the linked signal are written in the same synchronous block, the linked-signal write wins (it happens after the source update is observed):
this.tabs.set([newTabs]); // schedules linkedSignal reset
this.activeIdx.set(2); // overrides the reset
// activeIdx() is 2, NOT 0
This is intentional — user intent overrides automatic behavior. If you need the reset to win, write to the source AFTER any potential user write in the same tick.
Common gotchas #
| Symptom | Cause | Fix |
|---|---|---|
linkedSignal() doesn't reset when source changes |
The source isn't actually a signal (passed a raw value) | The source option must be a signal or a getter function returning one |
| Reset happens on every change detection | The computation has a non-signal dependency that re-evaluates | Make sure the computation only reads tracked signals |
| Equality doesn't fire | Default Object.is is comparing object identity, returns false on new object each time |
Pass a custom equal: function |
| Two linked signals fighting | Both writing to the same underlying source | One should be a computed() (read-only) instead |
Confused with model() |
linkedSignal is for derived state; model() is for parent-child two-way binding |
Different problems — pick the one that fits the shape |
What's next #
This closes Module 3. You now have every signal primitive Angular ships:
signal()— writable statecomputed()— read-only derived stateeffect()— side effects on signal changemodel()— two-way binding between parent and childlinkedSignal()— writable derived state with auto-resetresource()/httpResource()— async-backed signal-shaped state (lesson 6.3)debounced()/throttled()— signal helpers for rate limitingtoSignal()/toObservable()— bridges to RxJS
Module 4 picks up forms — signal forms first (the v22 modern API), then reactive and template-driven for migration cases. Lessons 4.2–4.6 build on the signal primitives.
Try it yourself #
A tab component using linkedSignal() to remember the user's choice when possible:
import { Component, input, linkedSignal, signal } from '@angular/core';
interface Tab { id: string; label: string; }
@Component({
selector: 'app-tabs',
template: `
<nav>
@for (t of tabs(); track t.id) {
<button [class.active]="t.id === activeId()" (click)="activeId.set(t.id)">
{{ t.label }}
</button>
}
</nav>
<p>Active: {{ activeId() }}</p>
`,
})
export class TabsComponent {
tabs = input.required<Tab[]>();
activeId = linkedSignal<Tab[], string>({
source: () => this.tabs(),
computation: (tabs, previous) => {
if (previous && tabs.some(t => t.id === previous.value)) {
return previous.value; // keep user's selection if the tab still exists
}
return tabs[0]?.id ?? ''; // fallback to first tab
},
});
}
Usage:
@Component({
template: `
<app-tabs [tabs]="visibleTabs()" />
<button (click)="hideOne()">Hide first tab</button>
`,
})
export class HostComponent {
allTabs = signal<Tab[]>([{id:'a',label:'A'},{id:'b',label:'B'},{id:'c',label:'C'}]);
visibleTabs = computed(() => this.allTabs());
hideOne() { this.allTabs.update(t => t.slice(1)); }
}
Click around the tabs — your selection sticks across re-renders. Click "Hide first tab" while you have a later tab active — the active stays. Click "Hide first tab" while it IS the active tab — the linked signal resets to the new first tab.
get_best_practicesUse linkedSignal whenever the dependency is straightforward: “on every change of source X, reset/recompute value Y, but allow user writes in between.” That’s exactly what linkedSignal is built for, and it doesn’t need allowSignalWrites: true. Reach for effect() with manual signal writes when the logic is more complex — multiple sources, conditional resets, side effects beyond just setting the value. The two are tools for the same family of problem; linkedSignal is the cleaner shape when it fits.Module 3 is complete. Module 4 picks up forms — starting with signal forms, the v22 modern API.
Up next in Angular
More from this topic
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.


