- Angular Gems by Vasileios Kagklis
- Posts
- Angular Signals - The Best Are Yet to Come
Angular Signals - The Best Are Yet to Come
A quick overview of the changes introduced by the Angular Signal API.
For so many years Angular relied on Zone.js to perform change detection. It’s been a love-hate relationship.
And just when you thought that there was no way out of this situation — it happened!
I’m talking about the new Angular Signal API. So many RFCs lately, yet so little time!
There was also a live discussion about the Angular Signal RFCs, with Q&A and all (only 2 hours long 😅).
If you are busy (or lazy like me), this article is for you! Here you will find all the highlights. Everything you need to know — the juice!
Let’s get started!
Disclaimer: I am not part of the Angular team. Although this article is based on official sources, some of the things may be subject to change in the future.
#1 — A new reactivity
As of version 15, Angular relies on Zone.js to perform change detection. This provides certain capabilities but also brings challenges.
The major issue is that Zone.js doesn’t provide accurate information about what changed in a component.
It only tells Angular that something changed. Angular has to then run change detection top-down the component tree to figure out where and what exactly changed.
Hence the need for a new reactivity.
The new reactive primitive aims at providing fine-grained information, which will lead to more efficient change detection, and thus more performant applications.
#2 — The Angular signal API
Signals are wrappers around values.
They are synchronous and behave like variables, but have an extra capability — they can notify when they’re changed.
We read their value through getters (e.g. price()
).
The main three concepts are signals, computed signals, and effects. We covered these extensively in our previous article.
But you shouldn’t have to read yet another article. So, here is a cheat sheet for quick reference:
UPDATE: Starting from Angular v17, the mutate function has been dropped from the signal API.
#3 — Switching between reactivities
Zone.js can’t and won’t be replaced overnight. You can’t just throw away all these years of relationship, can you? 😅
Jokes aside, this can’t happen because of backward compatibility.
Zones and signals are based on fundamentally different assumptions about how data flows through an application […]
There has to be a “bridging” point. A point where the reactivity model can switch seamlessly from zones to signals and vice versa.
The Angular team decided this switch should happen on the component level, which brings us to the next point.
#4 — Signal-based components
There is a new type of component coming in Angular — signal-based components. We will refer to components that use Zone.js as zone-based components.
Both types can be used in an application. Signal components can have zone components as children and vice versa.
To create a signal component, simply mark its class with signals: true
.
@Component({
signals: true,
// ...
})
export class MySignalComponent { }
#5 — Local change detection on view level
The fine-grained information (what changed and where) provided by signals enables local change detection.
But what does “local” refer to?
Local means that change detection will run on the view level, which is narrower in scope than a specific component.
A component’s template is a view, but there are also embedded views.
Views defined in an are called embedded views. Embedded views are not rendered until instantiated.
Directives such as NgIf
, NgSwitch
, and NgFor
all use ng-template
under the hood.
Each branch of an NgIf
or NgSwitchCase
and each row of an NgFor
are examples of independent views in Angular.
#6 — Signals in templates
Signal values are accessed through getter functions.
<p>Price: {{ price() }}</p>
However, we all know that calling a function in a template can cause performance issues.
Well, surprise!
This doesn’t apply to signals in signal components as the expressions will re-evaluate only when notified by the signal due to a change.
#7 — Signal inputs
In signal components, all component inputs are read-only signals.
@Component({
signals: true,
// ...
})
export class AccountInfoComponent {
// Optional input - no initial value.
firstName = input<string>();
// Input with a default value
lastName = input('Doe');
// Input with options
isActive = input<boolean>(false, { alias: 'disabled' });
// Output
checked = output<boolean>(); // EventEmitter<boolean>
}
So, we have two changes here:
how we declare them —
input
function instead of a decorator, andhow data flow through input properties — inputs are mutable in zone components which is not the case in signal components.
Outputs are declared with the respective output
function, but this is for consistency purposes. Internally, they still work the same as before.
#8 — Signal queries
In signal components, all queries are also signals.
By now it should be clear that signals are not in favor of decorators.
Therefore, the functions for signal queries are viewChild
, viewChildren
, contentChild
, and contentChildren
.
export class PandaListComponent {
input = viewChild<ElementRef>('search');
items = viewChildren(PandaItem);
}
#9 — New lifecycle hooks
New type of component means new lifecycle hooks.
Signal-based components retain only the ngOnInit
and ngOnDestroy
lifecycle methods.
Three (3) new lifecycle hooks are introduced — afterNextRender
, afterRender
, and afterRenderEffect
.
More specifically:
afterNextRender
schedules a function call after the next change detection cycle is complete.afterRender
schedules a function call after any time the framework performs DOM updates during rendering.afterRenderEffect
is a special kind ofeffect
that, when triggered, executes withafterRender
timing.
All other previous lifecycle hooks are not available, but signals provide new patterns to achieve the same end result. More specifically:
ngOnChanges
is for observing input changes. We can usecomputed
signals to derive values oreffects
to react to side effects.ngDoCheck
is for custom change detection. We can useeffects
instead.ngAfterViewInit
is often used for performing some action after initial rendering. We can useafterNextRender
instead.ngAfterContentInit
,ngAfterViewChecked
, andngAfterContentChecked
are often used for query results. Since queries are also signals, they can be used directly.
#10 — Observable and signal interoperability
RxJs observables are widely used in Angular applications. Somehow observables and signals must co-exist.
Fear not — there is a way!
We can convert an observable to a signal by using the toSignal
function.
const price: Signal<number> = toSignal(price$);
Under the hood, toSignal
subscribes to the given observable and updates the returned signal whenever the observable emits.
Respectively, we can convert a signal to an observable by using the toObservable
function.
const count: Observable<number> = toObservable(price);
So far, so good.
However, from their very nature, observables and signals operate differently:
An observable is like a stream of data. We need to subscribe to access the emitted values.
A signal is like a wrapper box that contains the value. This box knows when a read or write has happened (that’s what makes signals reactive).
Because of this, there are cases that require special handling. Let’s see some of them and how the new API deals with them.
Signals always have an initial value, but an observable may not always have one. For that, there is the initialValue
option:
const price = toSignal(price$, { initialValue: 0 });
Next, some observables emit synchronously and don’t require an initial value. This is covered by the requireSync
option:
const price$ = new BehaviorSubject(99);
const price: Signal<number> = toSignal(price$, { requireSync: true });
Lastly, the subscribe
method has three types of notifications — next
, error
, and complete
. For a signal created by the toSignal
function, the following apply:
The signal’s value is whatever is emitted from the
next
notification of the observable.When an
error
notification is received, the signal will throw that error the next time it is read.The concept of being “complete” doesn’t apply to signals. When an observable completes, the signal simply stops receiving new values from the stream.
Conclusions
Whew! Plenty of changes, but we covered the most important stuff. I hope that you got a clear view of what signals bring to Angular.
Thank you for reading!