The main use case
linkedSignal() is a perfect tool when you want to compute some initial value, then let the user modify it, and still keep it in a reactive signal.
export class CategoriesComponent {categories = input.required<Category[]>();
selectedCategory = linkedSignal(() => this.categories()[0]);
pickCategory(cat: Category) {
this.selectedCategory.set(cat);
}
}
How it works: the initial value of the selectedCategory signal will be computed as this.categories()[0] .
this.categories is a signal, and every time this signal changes, selectedCategory will be re-computed.
Technically, it is a bit more complicated: linkedSignal() will not re-compute its value at the moment when any of the signals it consumes notifies about a possible change, but rather when a consumer of the linkedSignal() reads it and the linkedSignal() has unchecked notifications about changes. However, stating this every time would make it much more difficult to read.
Those who are familiar with computed() might say that linkedSignal() works the same, but there are two important differences between them.
Look at the pickCategory() method: Here we set the new value for the selectedCategory signal, and this value will remain until the categories signal “emits” a new value — then selectedCategory will be recomputed again, and the value we previously set in that method will be overridden.
It is very useful for:
- displaying pre-computed forms (using previously saved data or default values) and letting the user change the values (to save them later, or just to affect other parts of the app);
- pages where the initial properties should be loaded from the API or default values, not the URL, and the URL cannot be the source of truth (large tables with filters and pagination, games, 3D scenes), and the user is supposed to modify the values;
- components where one input control might restrict the range of allowed values of other input controls. For example, a date picker where the user picks the year, month, and date separately;
- computed (derived) signals that should know the previously computed value.
And that is the second important difference: linkedSignal() not only returns a WritableSignal, but also lets us use the previous value.
Example:
export class VehiclesComponent {showAtv = linkedSignal<boolean, boolean>({
source: () => this.store.hasATVInfo(),
computation: (source, previous) => {
if (source === false) {
/**
* If other fields of the form were changed and
* we no longer have ATV vehicle info,
* but we are displaying it as "yes,"
* leave it as "yes" - do not reset this switch.
* Otherwise, the opened form will be unexpectedly
* (for the user) closed, and the user might still
* want to edit this form, bringing in the new ATV info.
*/
if (previous?.value === true) {
return true;
}
}
return source;
}
});
}
Sometimes you don’t need a WritableSignal, and you would like to use computed(), but you absolutely need the previous value to compute the new one, even if computed() is supposed to be pure. It’s not an idiomatic example, but for these exceptional cases, you can use this trick:
export class Example {someInput = input<string>();
myComputedSignal = linkedSignal<string, string>({
source: this.someInput,
computation: (source, previous) => {
if (previous?.value === 'SOME VALUE') {
return 'SOMETHING ELSE';
}
return source;
}
}).asReadOnly(); // <- and it's not writable anymore
}
With great power…
When we have more than one source of truth, things become less declarative and less safe. We should use linkedSignal() with care.
Look at this example:
If a user doesn’t want to be named “Goblin,” they will enter something else, for example, “Wizard.” Now they see in the form that their favorite color is Green and their nickname is “Wizard.” But their favorite color is purple, they input “Purple,” and now they are named “Purple Goblin.” Not an expected behavior, to say the least!
In this form, after “nickname” was edited, it should not be derived (computed) anymore.