Let me show you some tricks you can use to add reactivity to your templates, even when it looks impossible.
I’ll show you how to make any field of an object reactive, and you will not even need the async pipe to get the updates.
Let’s start with a simple template:
<ul>
@for(item of items; track item.id) {
<li>{{ item.name() }}: {{ item.id }}</li>
}
</ul>
Here is the type of “item” objects:
type Item = {
name: () => string;
id: number;
}
As you can see, name
can be updated synchronously, id
can not be updated at all.
But what if I tell you, that both of them can be reactive? And you can update them any way you need: synchronously or asynchronously.
When Angular reads signals in a template, the template becomes a consumer of these signals. And whenever you update these signals, the template will be notified. It is a simple concept to grasp.
But there is more: when a template calls some function to get its value, and this function reads a signal — the template still becomes a consumer of that signal. And the level of nesting doesn’t matter! Only two things can prevent it: if you make an asynchronous call, or if you wrap your call with untracked()
.
Example:
{{ name() }}
$user = signal({name: 'John Doe'});
name() {
return [this.firstName(), this.lastName()].join(' ');
}
private firstName() {
return this.getNameParts()[0];
}
private lastName() {
return this.getNameParts()[1];
}
private getNameParts() {
// here we read a signal!
return this.$user().name.split(' ');
}
updateUser() {
this.$user.set({name: 'Jane Doe'});
}
Here you can see 3 levels of nested calls before we read the signal. Two levels of function don’t know anything about the signal. And still, because the last one reads the signal, all of them will be called by the template again, after the signal is updated.
Yes, the code of this example is not perfect — it was made intentionally, to illustrate the idea.
Asynchronous reactivity using synchronous function call
protected readonly items: Item[] = [];
private readonly repeat$ = timer(0, 2000);
private readonly $repeat = toSignal(this.repeat$, {
initialValue: undefined
});
constructor() {
for (let i = 0; i < 10; i++) {
const item: Item = {
name: () => 'Item ' + i.toString() + ':'.repeat(this.$repeat() ?? 0),
id: i,
};
this.items.push(item);
}
}
Here we have a source of asynchronous data: observable repeat$
.
We convert it into a signal, that can be read synchronously: $repeat
.
When we populate the items array, we assign a function to the name field, that reads the $repeat
signal every time it is called.
And because it is a signal, our template will become a consumer of this signal when name()
is called! After that, whenever $repeat is updated, our template will be notified.
The source of data is asynchronous, but you don’t need async pipe to get the update notifications.
Because Angular Signals match the signature of a callback () => T
, you can just use signals to add reactivity when a callback function is accepted as a value for a field.
Replacing fields with getters to make them reactive
Field id also can be reactive — we can replace it with a getter in our object, and this getter will read a signal:
private readonly $offset = signal<number>(0);
constructor() {
for (let i = 0; i < 10; i++) {
const $id = computed(() => i + this.$offset());
const item: Item = {
name: () => 'Item ' + i.toString() + ':'.repeat(this.$repeat() ?? 0),
get id() {
return $id();
},
};
this.items.push(item);
}
}
Now, when the template reads id, it becomes a consumer of $id
— a signal, created using computed()
, which reads the signal $offset
. Whenever $offset is updated, $id
is notified and this signal notifies the template.
This trick might become very useful if you need to add reactivity to some part of an existing template, and migrating it to signals or observables is too expensive or too dangerous.
Combining Signals with Observables
Quite often we need more complicated sources of asynchronous data, than just timer()
.
The most common source of asynchronous data is HTTP requests. Let me show you how you can easily combine Signals and Observables to get the ultimate power.
Template:
<p>Current temperature in Barcelona: {{temp()}}</p>
<button (click)="readTemp()">Check Temperature</button>
Component:
private readonly reloadTemp$ = new Subject<void>();
private readonly $tempIsLoading = signal<boolean>(false);
private readonly http = inject(HttpClient);
readTemp() {
this.reloadTemp$.next();
}
// observable becomes a signal
private readonly $weather = toSignal(
this.reloadTemp$.pipe(
startWith({}),
switchMap(() => {
this.$tempIsLoading.set(true);
return getWeather(this.http).pipe(
finalize(() => this.$tempIsLoading.set(false))
);
})
),
{ initialValue: undefined }
);
// consumer of $weather
private readonly $temp = computed(() => {
if (this.$tempIsLoading()) {
return 'Checking...';
}
const w: any = this.$weather();
if (!w) {
return 'Error';
}
return w.current.temperature_2m + '℃';
});
private getTemperature() {
return this.$temp();
}
protected temp() {
// just to illustrate the fact,
// that this function does not know
// anything about the signal it will use.
return this.getTemperature();
}
You can find the full code of all these examples and play with this app: StackBlitz.
Using tricks like these, you can add reactivity to the places where it was impossible to start thinking about, and most of the time you will not even need to modify a template.
🪽 Do you like this article? Share it and let it fly! 🛸
💙 If you enjoy my articles, consider following me on Twitter, and/or subscribing to receive my new articles by email.
Top comments (0)