In this article, I share my experience of working with Angular Signals after almost a year of using them.
In Angular templates, Signals are better than Observables: they schedule Change Detection without any pipes, they are glitch-free, and you can read the same signal multiple times and it will be “free” in terms of performance (and read values are guaranteed to be the same). There are other reasons that are not so easy to explain briefly, but that’s already enough to make a rule: every variable (that might change) in your new templates should be a Signal.
Outside of templates, Signals also can be used for reactivity, but, as I mentioned, without a time aspect.
I once wrote a post on Twitter about it, and now I’ll post it here, updated and improved:
There are two ways to create reactive variables in Angular: Observables and Signals. If you describe in words, how your variable should express its reactivity, you’ll see what you need to use.
If the role of a variable can be described as conditions, then you need a Signal:
If the description of a variable’s role includes words, related to time, you need an Observable:
Signals have no time axis, and they can not delay a value — they always have a value, and their consumers should be always able to read it.
Consumers of Signals, computed()
, effect()
, and templates do not guarantee that they will read every new value written to the Signals they watch. An updated Signal will be eventually read, not instantly after the update as it happens with Observables. Consumers decide when they will read the new value using their scheduling mechanisms. It might be “in the next task,” “during the next Change Detection cycle,” or at some other moment, up to the consumer.
computed()
?Whenever you like!
computed()
is the best thing in Angular Signals, incredibly handy and safe to use. Using computed()
, you’ll make your code more declarative (you can read more about it in this article).
There are just two rules about the usage of computed()
:
computed()
. It should compute a new result, that’s it. Do not modify the DOM, do not mutate variables using this
, and do not call functions that might do that. Do not push values to Observables — it will cause unintentional reactive context propagation (explained below for effect()
). computed()
should not have side effects, it should be a pure function.computed()
. This function does not allow modification of Signals (and it is amazingly helpful), but it can not track asynchronous code. Moreover, Angular Signals are strictly synchronous, so if you want to use asynchronous code in computed()
, you are doing something wrong. So, no setTimeout()
, no Promises, no other asynchronous things.effect()
?Angular docs say that you’ll rarely need effect()
and discourage you from using it (copy, if docs will be edited).
And that info is correct: you rarely need effect()
… if your code is declarative ;)
The more imperative your code, the more often you’ll need effect()
. There is no code without imperative parts, but we all should try to make our code as declarative as possible, so we do need to use effect()
as rarely as possible.
Besides the dangers, mentioned by Angular docs (infinite loops, change detection errors), there is another thing, that might be quite nasty: effects are executed in a reactive context, and any code you call in effect, will be executed in a reactive context. If that code reads some signals, they will be added as dependencies to your effect. Here Alex Rickabaugh explains the details.
I still don’t want to encourage you to use effect()
, but I’ll give you advice on how to use it as safely as possible:
effect()
should be as small as possible. This way it will be easier to read and spot erroneous behavior.untracked()
:effect(() => { // reading the signals we need const a = this.a(); const b = this.b(); untracked(() => { // rest of the code is here - this code should not // modify the signals we read above! if (a > b) { document.title = 'Ok'; } }); });
More information about cases where untracked()
helps can be found in this article.
…is ok!
Your code will have Signals and Observables, at least because Signals can not be used for every kind of reactivity (see detailed explanation above). It is not an issue, it is perfectly fine.
If you need some value from an Observable in your computed()
, create a Signal using toSignal()
(outside of computed()
).
If you need to read a Signal in Observable’s pipe()
, there are two ways: