Understanding Angular's Linked Signal
Angular's signal-based reactivity system offers robust primitives like signal, computed, and effect to handle state in a reactive manner.
However, there were specific edge cases that felt like something was missing from the toolkit... until now!
Before linked signals, certain scenarios were tricky to implement using a signal-based approach.
It wasn't that these scenarios were impossible with signals.
But the code required for those particular situations was often verbose and awkward, and it simply didn't feel clean.
Fortunately, linked signals address many of these edge cases in a far more graceful way.
In this article, we'll take a detailed look at what a linked signal is, the appropriate contexts for its use, and the reasoning behind it.
What Exactly is a Linked Signal?
A linkedSignal is a new addition to Angular's signal API that enables a writable signal to maintain a connection with one or more source signals.
While a computed signal is read-only, a linkedSignal preserves a reactive link to its sources but remains writable.
Core Characteristics of linkedSignal:
These are the main attributes that define linkedSignal:
- Writable: In contrast to
computed, which is read-only, alinkedSignalcan be modified directly through thesetorupdatemethods. - Reactive: Similar to
computed, it obtains its value from one or more source signals.
Let's examine some code to see this in action.
A Practical Example
Consider this straightforward example:
const quantity = linkedSignal({
source: () => ({ courseCode: this.selectedCourse() }),
compute: ({ courseCode }, previous) => {
const course =
this.courses.find(c => c.code === courseCode);
return
course ? course.defaultQuantity : previous ?? 1;
}
});
The fundamental parts of a linked signal to understand are:
- source: The source signal from which the linked signal derives its value.
- compute: The function responsible for calculating the linked signal's value.
Here, quantity is a linked signal that gets its value from the selectedCourse source signal.
Here's a more detailed breakdown of the process:
- The
sourcefunction establishes which signals the linked signal is connected to. - The
computefunction is triggered whenever any of these source signals emit a new value. computereceives both the latest value of the source signals and the previous value of the linked signal.- It then returns the new value for the linked signal, which is passed on to any consumers.
In this scenario, the quantity linked signal will be refreshed every time the selectedCourse signal changes.
This sounds a lot like a computed signal, doesn't it?
The key distinction is that, unlike computed, a linked signal allows for writing.
You can modify the value of a linked signal using the set or update methods, as shown here:
quantity.set(20);
quantity.update(current => current + 1);
As we can observe, linkedSignal represents a fusion of the computed and signal primitives into a single entity.
That's the core concept, but you're probably still wondering about its practical applications and benefits.
Scenarios Where Linked Signals Shine
To grasp the utility of linked signals, let's work through a simple yet realistic example.
Imagine a shopping cart where users can add items and specify the quantity for each one.
Each item in the list comes with a default quantity.
When the user selects a different product to add, the quantity should revert to that product's default.
Here's a possible HTML structure for the shopping cart component:
<div class="demo-container">
<h1>Shopping Cart</h1>
<div class="form-control">
<label>Select Course</label>
<select [value]="selectedCourse()"
(change)="onCourseSelected(course.value)" #course>
@for (course of courses; track course.code) {
<option [value]="course.code">
{{course.title}}
</option>
}
</select>
</div>
<div class="form-control">
<label>Quantity</label>
<input type="number"
[value]="quantity()"
(change)="onQuantityChanged(input.value)"
#input/>
</div>
<div class="form-actions">
<button class="btn"
(click)="onArticleAdded()">
Add To Cart
</button>
</div>
</div>
Let's dissect what's happening:
- All state is managed with signals: the selected course, the quantity, and the courses list are all signals.
- The user can pick a course from a list to add it to the cart.
- The quantity for the item is shown in an input field.
- This input field is connected to the selected course.
- Whenever the selected course changes, the quantity field resets to that course's default.
The quantity signal is the crucial piece here.
Tt must reset to the selected course's default each time the user makes a new selection from the dropdown.
At the same time, it needs to be writable so the user can manually adjust the quantity.
Implementing This Scenario
One option is to use a computed signal to derive the quantity based on the selected course.
However, computed signals are read-only, which means we couldn't reset the quantity to the default requirement.
Another approach is to use an effect to set the quantity whenever the selected course changes.
Here is what that might look like:
effect(() => {
const selectedCourse = this.selectedCourse();
quantity.set(
courses.find(
c => c.code ===
selectedCourse)?.defaultQuantity ?? 1);
});
It works, but does it?
The Problem with Using an Effect
This approach does function, but it's somewhat inelegant.
It isn't very declarative, as we're imperatively setting the quantity signal's value within the effect.
Writing to signals inside effects is possible now, but it's still advisable to avoid it, as it can easily lead to infinite loops.
It's also not immediately clear that the quantity signal is derived from the selected course signal, just from looking at the declarations.
It's not that it couldn't work, but it just seems wrong.
Effects are intended for handling side effects, not for this kind of signal-to-signal value computation.
Generally, it's best to minimize the use of effects and treat them as a last resort.
The Better Solution
This is precisely where linkedSignal comes into play.
Here's the complete component, but this time using a linked signal:
@Component({
selector: 'linked-signal-demo',
templateUrl: './linked-signal-demo.component.html',
})
export class LinkedSignalDemoComponent {
// these are the products of the shopping cart
courses = [
{
code: "BEGINNERS",
title: "Angular for Beginners",
defaultQuantity: 10
},
{
code: "SIGNALS",
title: "Angular Signals In Depth",
defaultQuantity: 20
},
{
code: "SSR",
title: "Angular SSR In Depth",
defaultQuantity: 30
}
];
// this is the product that the user has
// selected to add to the cart
selectedCourse =
signal<string | null>("BEGINNERS");
// this is the quantity of the product that the user
// has selected to add to the cart
// it needs to be reset to the default quantity
// of the last selected product,
// when the user changes the selected product
quantity = linkedSignal({
source: () => ({courseCode: this.selectedCourse}),
computation: (source, previous) => {
return
this.courses.find(c => c.code
=== source.courseCode())?.defaultQuantity ?? 1
}
});
onQuantityChanged(quantity: string) {
this.quantity.set(parseInt(quantity));
}
onCourseSelected(courseCode: string) {
this.selectedCourse.set(courseCode);
}
}
As you can see, linkedSignal() handles this scenario in a much more elegant and declarative manner.
The dependency between the selectedCourse and quantity signals is now clearly visible, all from the signal declarations, which is excellent.
Also, the quantity signal remains writable, allowing for manual changes by the user.
If the selected course changes, the quantity will automatically reset to the default for that new course.
This achieves the desired outcome without needing any effects!
Should You Use linkedSignal Everywhere?
That's a fair question.
The answer is that linked signals aren't intended to replace computed signals, plain signals, or effects.
They are a new primitive designed to fill a particular void in the signal API.
They aren't meant for general use, but rather for specific and relatively uncommon instances where you need both reactive updates and value modification simultaneously.
If a computed signal can solve your problem, there's no need for a linkedSignal.
If you only need a writable signal, a plain signal, which is writable by default, should suffice.
Linked signals should be used sparingly.
In the majority of cases, the signal and computed primitives are sufficient to address the vast majority of issues and should be our primary tools.
Typical Use Cases for Linked Signals
Here are a few common situations where linked signals might come in handy:
- Resetting a form field based on the value of another field.
- Situations in state management where values need to reset reactively but still allow for user overrides.
- When computed signals are too limiting, but effects feel too imperative, a linkedSignal might be a better fit.
Final Thoughts
The linkedSignal primitive addresses a significant gap in Angular's signal-based reactivity model.
It enables developers to create reactive relationships between signals that are also writable, which is ideal for cases like field resets and other related scenarios.
While it certainly isn't meant to replace computed or signal, it offers a powerful alternative when you need writable computed values.
If you're using Angular's signal API, give linkedSignal a try — it can make your code cleaner and easier to maintain.
But remember to rely on the computed and signal primitives for most problems, as they are more than adequate for the majority of situations.
For more comprehensive content on signals, check out the Modern Angular with Signals Course at the Angular University.
