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:

#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>

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:

  1. how we declare them — input function instead of a decorator, and

  2. how 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 of effect that, when triggered, executes with afterRender 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 use computed signals to derive values or effects to react to side effects.

  • ngDoCheck is for custom change detection. We can use effects instead.

  • ngAfterViewInit is often used for performing some action after initial rendering. We can use afterNextRender instead.

  • ngAfterContentInit, ngAfterViewChecked, and ngAfterContentChecked 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!