Understand How to Use Angular Signals: A Comprehensive Deep Dive

Hitesh Mandav
7 min readJun 13, 2023
Angular Signals banner

The article aims to explore the features and benefits of Angular Signals, showcasing code examples and practical use cases.

Readers can expect a comprehensive overview of Angular Signals, including their types, usage, and advanced functionalities. The article also discusses the interoperability of Angular Signals with RxJS observables, providing a holistic understanding of how to leverage both technologies for reactive programming.

By the end of the article, readers will clearly understand Angular Signals and how they can enhance their Angular projects.

Why do we need Signals in Angular?

Last year, the Angular team embarked on an ambitious long-term research project, aimed at enhancing runtime performance and providing more flexibility by making zone.js optional. As part of the Angular 16 developer preview, a groundbreaking feature called Angular Signals has been introduced. Signals revolutionize the core workings of Angular by offering fine-grained reactive capabilities.

Currently, Angular relies on change detection, along with the assistance of zone.js, to update the DOM whenever there are changes in the application state. However, this approach has its limitations. Automatic change detection with zone.js often leads to over-checking the component tree, resulting in a performance deficit.

Furthermore, one common issue is the dreaded ExpressionChangedAfterItHasBeenCheckedError, which stems from the current change detection mechanism. Although the OnPush change detection strategy can mitigate the performance deficit, it requires additional effort and attention from developers.

While RxJS is excellent for managing streams of data, it falls short when it comes to handling synchronous current values. Although BehaviorSubjects in RxJS come close to mimicking signals for managing current values, they transform into observables when piped through an operator, limiting their suitability for this purpose.

To address these underlying challenges, Angular Signals come into play.

Understanding and Using Angular Signals

Signal is a reactive primitive that is provided as a part of @angular/core and can help notify any consumer when its value changes. Signals can contain any value, from simple primitives to complex data structures.

With signals, components no longer need to be aware of each other’s existence to exchange information or trigger actions.

There are two types of signals: writable signals and read-only signals.

To create writable signals, you can initialize a signal with a value and read the value by calling the created signal as a function.

import { Signal } from '@angular/core';

const count: Signal<number> = new Signal(0);

// Signals are getter functions - calling them reads their value.
console.log('The count is: ' + count());

Writable signals have three ways to update their value: .set(), .update(), and .mutate():

// Set a new Value
count.set(3);

// Change value based on previous value.
count.update(value => value + 1);

const wishList = signal([{title: 'PS5', price: '499'}]);

wishList.mutate(wishList => {
// update the current value by mutating it in place.
wishList.push({title: 'XBOX SERIES X', price: '499'})
});

All three methods will notify any consumers of the change in value.

Signal consumers can be created using effect and computed functions.

Computed function takes a derivation function as an argument and returns a read-only signal. The derivation function can use one or more other signals on which the value of the computed signal is dependent.

Here’s an example that demonstrates computed signals:

  cartItems = signal<CartItem[]>([
{
product: {title: 'PS5', price: '499'}
quantity: 3
},
{
product: {title: 'XBOX SERIES X', price: '499'}
quantity: 2
},
]);

// Total up the extended price for each item
subTotal = computed(() =>
this.cartItems().reduce(
(a, b) => a + b.quantity * Number(b.product.price),
0
)
);

// Delivery is free if spending more than 100,000 credits
deliveryFee = computed(() => (this.subTotal() < 2000 ? 99 : 0));

// Tax could be based on shipping address zip code
tax = computed(() => Math.round(this.subTotal() * 10.75) / 100);

// Total price
totalPrice = computed(
() => this.subTotal() + this.deliveryFee() + this.tax()
);

The computed signals are lazily evaluated and memoized. This means that the subTotal signal depends on cartItems values, but the subTotal value is calculated only when subTotal is first read, and then this value is cached for future use without recalculation. However, if any of the cartItems are updated, the cached value subTotal becomes invalid. The recalculation will happen only when subTotal is read, and not every time the cartItems value changes.

Since computed signals are read-only, we cannot perform functions like set, update, or mutate on them to change their values.

Effect is another consumer of a signal. An effect can help in performing asynchronous operations on signal changes. The effect function also takes a function as an argument that can contain one or more signals, and their values can be used to perform asynchronous tasks.

Here’s an example of an effect:

// Set SearchTerm from localStorage.
const searchTerm = signal(localStorage.getItem('searchTerm') || '');

// Update localStorage value every time serchTerm is changes by user.
effect(() => localStorage.setItem('searchTerm', this.searchTerm()));

By default access to inject function is required to register a new effect. The easiest way is to call the effect function in a constructor of a Component directive or service.

Alternatively, the effect can be assigned to a field.

private logger = effect(() => console.log(this.searchTerm()));

Both of these ways of calling an effect function will cause the effect to occur once the component is initialized. What if we want the effect to start executing only on certain user actions? So, for such scenarios where the effect cannot be declared in the constructor or a field, we can pass the Injector to the effect via its option.

this.logger = effect(
() => console.log(this.searchTerm()),
{
injector: this.injector,
});

we can also destroy effects using .destroy() for manual cleanup.

this.logger.destroy();

Effects might start long-running operations, which should be canceled if the effect is destroyed or runs again before the first operation finishes. When you create an effect, your function can optionally accept a onCleanup function as its first parameter. This onCleanup function lets you register a callback that is invoked before the next effect run begins or when the effect is destroyed.

import { signal, effect } from '@angular/core';

effect((onCleanup) => {
const user = currentUser(); const timer = setTimeout(() => {
console.log(`1 second ago, the user became ${user}`);
}, 1000); onCleanup(() => {
clearTimeout(timer);
});
});

Deep Dive in Angular Signals

Angular Signals offer a range of advanced features, such as defining custom equality functions when creating signals, reading without tracking dependencies, and cleanup functions for effects. These features provide additional flexibility and control when working with signals.

Signals Equality function: When creating a signal, we can optionally provide an equality function that will be used to check whether the new value is different from the previous one. Equality param can be provided to both computed and writable signals.

import _ from 'lodash';

const data = signal(['test'], {equal: _.isEqual});

// Even though this is a different array instance, the deep equality
// function will consider the values to be equal, and the signal won't
// trigger any updates.
data.set(['test']);

One thing to keep in mind while using the equality parameter is that it is not considered in the case of .mutate() being performed since it changes the current value without producing a new reference

Reading without tracking dependencies: SSometimes, we may want to notify a consumer only in the case of a single signal change rather than all the values used in its derivative function. Although this might be a rare scenario we can use untracked() to achieve this.

effect(() => {
console.log(`User set to `${currentUser()}` and the counter is ${counter()}`);
});

Here the effect executes any time either the currentUser or the counter updates. But what if we only want to log counter in when the currentUser changes and not log the currentUser every time counter changes?

effect(() => {
console.log(`User set to `${currentUser()}` and the counter is ${untracked(counter)}`);
});

This will now untrack the counter as a dependency for this effect function.

RxJS Interop for signals

RxJS and Angular Signals can be very powerful together for reactive programming. While signals are great for synchronous reactive programming, observables are a powerful asynchronous reactive programming tool. Angular provides the @angular/core/rxjs-interop package, which offers useful utilities to integrate Signals with RxJS observables. It provides toSignal and toObservable methods to convert observables to signals and signals to observables, respectively.

In summary, Angular Signals is a cool feature that can make your Angular apps faster and more responsive. They work by helping your app know when things change and updating only what needs to be updated, without slowing down the whole app.

Throughout this article, we have learned about the different types of Angular Signals and how they can be used. We’ve also seen how they can work together with RxJS observables to make your app even more powerful.

If you want to give signals a quick try, head to StackBlitz, create a new Angular project, and change the dependencies in package.json to the latest pre-release version of Angular 16. Try out signals or fork the Angular Signals Example project.

Thank you for taking the time to read this article! I would love to hear from you in the comments section if you have any questions or suggestions. Your support is greatly appreciated, so please don’t forget to give this article a 👏 to help spread the knowledge to more people. For more insightful articles in the future, make sure to follow me on Medium. Happy coding!!!

--

--