February 5, 2024
Angular Signals Part 1 – How-to guide on Angular Signals
Klicke hier für die deutsche Version des Blog-Posts.
Enhancing Performance in Angular 17 with Signals: A Smart Solution
Which problem is solved by signals?
Angular’s Signals are engineered to elevate application runtime performance by replacing Zone.js. Traditionally, Zone.js played a crucial role in activating Change Detection and refreshing the UI whenever the application’s state changed. This method required scanning the entire component tree to identify relevant state changes, often leading to performance lags due to redundant checks on inactive components.
The introduction of Signals renders the comprehensive component tree scans unnecessary. Now, only the components directly impacted by a change are updated, significantly streamlining the DOM update process. This focused strategy not only minimizes system overhead but also boosts the application’s overall efficiency.
Beyond enhancing performance, Angular’s Signals also simplify state management, offering a more intuitive alternative to RxJS, especially when replacing Subject-based state. RxJS, with its powerful but complex set of tools for reactive programming, can be challenging for developers to master, especially when managing application state through Subjects. Subjects in RxJS, while flexible, require a deeper understanding of reactive programming concepts, such as observables and subscribers, making them less accessible for beginners or for simpler applications.
In contrast, Signals provide a straightforward mechanism for state management. They eliminate the steep learning curve associated with RxJS by offering a simpler, more direct approach. Developers can define a state and directly link it to the UI components, bypassing the complexities of observable streams. This direct linkage simplifies the process of updating the UI in response to state changes, as there’s no need to manage subscriptions or handle streams of data. With Signals, state management becomes more about defining and reacting to state changes, rather than juggling the intricacies of reactive programming paradigms.
Signals aka Reactive Primitives
Signals, known as Reactive Primitives, are a system that tracks the use and dependencies of state within an application, enabling Angular to optimize rendering updates. With Signals, Angular precisely identifies where state is used and its dependencies. This allows for targeted re-rendering of components, reducing the need for exhaustive checks and eliminating the reliance on Change Detection. Unlike Observables, Signals don’t require subscriptions and always hold an initial value, simplifying state management by removing the need for asynchronous handling, such as the async pipe.
Signals are thoroughly typed and can be of type Number or String or even complex types. They can be writeable or readonly and you can always create a readonly signal from any writeable signal with .asReadonly()
. Readonly signals can also depend on writable signals but we will look at that later.
It’s also very easy to export signals in order to use them in several components.
1 |
export const name = signal('Angular'); |
1 |
import { name } from "main"; |
How to interact with Signals
So now that we’ve established the benefits of Signals in tackling common challenges in Angular development, it’s time to delve deeper. Let’s explore how to utilize them effectively and examine the various methods of interaction available at our disposal.
signal()
By calling the signal
function you can create a writeable signal. In our case, a counter.
1 |
const counter = signal(0); |
We can access the value with the variable name and round brackets. This also works in the template if you use it in an expression.
1 |
console.log('New counter value', this.counter()); |
1 |
Count: {{ counter() }} |
set()
With set
you can give the signal a new value.
1 |
this.counter.set(5); |
We can use this to reset the counter to zero.
1 2 3 |
reset() { this.counter.set(0); }; |
update()
By using update
you can also change the value of the signal, but now you have access to the current value. So you can set a new value based on the old one.
In our example we can use this for an increment or decrement function.
1 2 3 |
increment() { this.counter.update((currentValue) => currentValue + 1); }; |
The update
function doesn’t have to be a one-liner. As long as you specify a return value, you can perform various operations.
1 2 3 4 5 6 7 |
decrement() { this.counter.update((currentValue) => { console.log('Old value', currentValue); const newValue = currentValue - 1; return newValue; }); }; |
computed()
To create a signal that is based on or dependent on another signal you can use computed
. This function generates a readonly signal that updates itself if the value of the signal on which it depends changes.
As an example we will make a variable that checks if the current counter is even or odd. isOdd
cannot be changed through the set
or update
function. But since Angular knows that there is a dependency between the two signals, every time the counter
signal changes the callback function of isOdd
is executed again.
1 |
const isOdd = computed(() => this.counter() % 2 === 1); |
Of course this also works with two or more signals to depend on. Now combined
is updated every time either firstLetter
or secondLetter
changes.
1 2 3 |
const firstLetter = signal('a') const secondLetter = signal('b') const combined = computed(() => this.firstLetter().concat(this.secondLetter())); |
As with the update
function computed
doesn’t have to be one line. But be aware that the dependencies of a computed signal are not only determined by its return value. In the example below combined
now only uses firstLetter
in the return statement, but it’s still getting updated when the value of secondLetter
changes since the signal is used in the callback function of combined
.
1 2 3 4 |
const combined = computed(() => { console.log('Second letter changed', this.secondLetter()); return this.firstLetter().concat('c') }); |
effect()
With effect
you can declare what should happen if the value of a signal changes or in other words which side effects are triggered by this. That can be logging the value of a signal, exporting the value to localStorage or saving the value transparently to the database.
In our case we just want to print the new counter value to the console.
By default, registering a new effect with the effect
function requires an injection context (access to the inject function). The easiest way to provide this is to call effect
within a component, directive, or service constructor. Alternatively, the effect can be assigned to a variable (which also gives it a descriptive name).
1 2 3 4 5 |
constructor() { effect(() => { console.log('New counter value', this.counter()); }); } |
Like the computed
function an effect can have a dependency to multiple signals.
1 |
const letterEffect = effect(() => console.log('Log letters', this.firstLetter(), this.secondLetter())); |
untracked()
If you want to read signals in a reactive function such as computed
or effect
without creating a dependency you can prevent a signal read from being tracked by calling it with the untracked
function.
Let’s take the letterEffect
from above as an example. At the moment the effect logs the current letters when either one of the signal values changes. If the effect should only be triggered when the firstLetter
changes, but not when the secondLetter
changes we can write the following:
1 |
const specialEffect = effect(() => console.log('Special effect', this.firstLetter(), untracked(this.secondLetter))); |
Experience is the Best Teacher: Hands-On with Signals
Ready to put theory into practice? In this interactive StackBlitz example, we’ve crafted a sample app that embodies the Signal concepts we discussed. Feel free to dive in and experiment to see Signals in action!
Use cases of Signals
Signals are well suited to manage a synchronous state in the components. They don’t for events and other asynchronous operations. Therefore signals are an extension to RxJS and not a replacement. They can be a substitution for async pipes and OnPush components as Angular notices by itself when something in the component changed.
More about the usage of Signals and RxJS will be covered in part two of our angular signals article series.
If you want to dive deeper into the what, why and how of Angular Signals we really recommend the Videos of Deborah Kurata. She does an exceptional job in explaining the concepts behind the new reactive primitive.