The ability to create stable and feature-rich web applications has made Angular, a potent JavaScript framework, extremely popular. Managing states becomes a crucial part of development as applications become more complex. Angular Signals can be used in this situation. Within Angular applications, signals offer a method for effectively managing state and improving rendering updates.
Developers can take fine-grained control over state changes by utilizing Signals, which enables Angular to optimize rendering updates for faster performance and a seamless user experience. We will examine the foundations of Angular Signals in this article, along with their types and applications. We will delve into real-world scenarios and code snippets to demonstrate the advantages of utilizing Angular Signals for state management and rendering optimization in our Angular applications. Let's get started and see how Signals can transform the way we manage state and optimize rendering updates, resulting in Angular applications that are more effective and reactive.
It is important to note that the Angular signal is a new update that is usable only when we have the new Angular 16 version installed or running.
Prerequisites
Hands-on experience with Angular
Basic knowledge of Angular's Reactive principles
Importance of Signals in Angular Apps
Angular Signals play an important role in state management and rendering optimization in Angular applications, providing distinct advantages over other reactive primitives such as RxJS and Co. The importance of Angular Signals lies in their lazily evaluated and memoized computed Signals, automatic dependency tracking, simplified state updates, granular state tracking, and change detection integration. Signals give developers granular control over state changes, allowing for more effective and focused state management.
The writable Signals make updating state values simpler without requiring difficult operations. By only launching necessary updates, automatic dependency tracking optimizes rendering performance. Computed signals that have been hastily evaluated and memorized save time by avoiding unnecessary calculations. The efficient updates in OnPush
components are ensured by the integration with Angular's change detection. Angular Signals is a useful tool for state management and rendering optimization in Angular applications because of these benefits.
What are Angular Signals?
We appear to have begun defining what angular signals are in the preceding sections, but let us first define what a signal is. Assume we are in a room with people who cannot hear or speak, and we can only communicate with them by using signs with our hands and expression. For example, if we need to tell them to stand on their feet, we do so by sequentially moving our hands upwards, they get the sign, and then stand on their feet, and so on. We have signaled to them with our hands, causing them to change their current state from sitting to standing.
Angular signals work in the same way; we have a variable written as a signal, and when we update it, the signal catches it and automatically updates the variable, resulting in reactivity. Signals in angular, on the other hand, are variables that store a variable and provide a signal or notification whenever the variable is updated.
How to create and use Angular Signals
Now that we know what signals are and why they are important, we can look at how to use them in our angular applications. In its version 16 release, the angular team included three (3) reactive primitives (signals):
Writable Signals
Computed Signals
Effects
These are the primary primitives for using Signals to achieve reactivity in angular applications. However, let us go a step further and learn how to combine and use the aforementioned primitives.
We can check out this article's Stackblitz project here for a better understanding.
1. Writable Signals
Writable signals are Angular signals whose values can be directly updated. They are essentially pre-defined signals. A signal must be defined or created before it can be written to, updated, or read. To have a writable signal, we must first define it. However, defining or creating a signal is not a difficult process; consider the code below to see how a signal is defined.
// defination of a signal
our_first_signal = signal<number>(0);
The above line of code is the technical demonstration of how to define or create a signal. We create a property named our_first_signal
, we then assign the signal()
function, provided by the angular team with an initial value of 0, hence making the property our_first_signal
a signal and not a normal property.
Generally, signals are getter functions hence, to read the value of our defined signal we would need to call the signal like we would call a function. The lines of code below demonstrate how to read the value from a signal.
//reading the value of signal
ngOnInit(): void {
console.log("Our first signal:", this.our_first_signal())
// returns 0
}
In the above line of code, we have a console.log
statement that logs the value of the signal that we are reading by calling it.
So far we have defined a signal and read its value. Now let us get to the interesting part of updating or writing to the signal. Let us take for example we want to update the value of our signal from 0 to 5, how do we go about this? Thanks to the angular team, we can use the set()
or update()
method to change or update our signal. The update()
method modifies the signal based on its current value while the set()
method changes the value of a signal. Let us examine the following lines of code below for a better understanding of how to use these methods:
ngOnInit(): void {
this.our_first_signal.set(8);
// or
this.our_first_signal.update((val) => val + 2);
console.log('Our first signal:', this.our_first_signal());
// expected results: 10
}
Using the set()
method, we set the value of our_first_signal
signal to 8, as shown in the code above. In the second line, we use the update()
method to add 2 to the value of our_first_signal
. The update method accepts a callback function as an argument, which is called with the current value of the signal, and the callback function's return value is set as the new value of the signal. The value of our_first_signal
is logged to the console in the third line.
It is important to note that signals also work with strings, numbers, arrays, and objects. When working with either arrays or strings, situations may require updating a particular property while maintaining other values in the array. To achieve this, the angular team has a mutate()
method that changes the content of a signal value rather than the value itself. Let us take for example we have an array of objects with properties name
and age
and we want to update the age
property while retaining the value of name
we would use the mutate()
method to do this. Let us carefully examine the lines of code below to understand the technical application of the mutate()
method.
// creating the signal
persons = signal(
[
{
name:"Alice",
age: 10
},
{
name: "John",
age: 15
}
]
);
// using the mutate() method to replace the first object's age to 21
this.persons.mutate(value =>
value[0].age = 21
)
// reading our signal by calling and logging it to the console
ngOnInit(): void {
console.log("personsSignal", this.persons())
}
We created a signal called persons
and assigned it an array of objects in the preceding code. We then used the mutate()
method to change the age
of the first object to 21. Finally, we called the signal and recorded it on the console. The console's output can be seen in the image below.
As we can see, the age of the first object has been replaced with 21. This is because the mutate() method is used to change the signal's value. A callback function is passed as an argument to the mutate() method. The callback function takes the current signal value as an argument and returns the new signal value.
2. Computed Signals: Getting Dynamic Values
Computed signals are a powerful tool in Angular applications for deriving values from other signals. When calculating derived values, computed signals are lazily evaluated and memoized, resulting in efficient and reactive behavior.
We use the computed()
function with a derivation function as its argument to create a computed signal. Let's look at some code samples to see how computed signals work.
// Create two signals: price and quantity
const price = signal(10);
const quantity = signal(5);
// Create a computed signal for total cost based on price and quantity
const totalCost = computed(() => price() * quantity());
ngOnInit(): void {
console.log(totalCost()); // Output: 50
}
We created two signals, price
, and quantity
, in the code above to represent the price and quantity of a product.
Following that, we define a computed signal named totalCost
. The total cost is calculated by multiplying the values of the price and quantity signals. Only when the computed signal is read for the first time or when its dependencies (price
or quantity
) changes will it perform this calculation.
Finally, we log the value of totalCost
to the console, which in this case is 50 based on the initial price and quantity values.
The ability of computed signals to handle complex calculations and transformations is a notable advantage. Consider the following scenario: we have a signal that represents an array of products, and we want to compute the total value of all products:
// Create a signal for the array of products
products = signal([
{ name: 'Product A', price: 10 },
{ name: 'Product B', price: 15 },
{ name: 'Product C', price: 20 },
]);
// Create a computed signal for total value based on products
totalValue = computed(() =>
this.products().reduce((sum, product) => sum + product.price, 0)
);
//reading the signal
ngOnInit(): void {
console.log("computed2",this.totalValue()); // Output: 45
}
In this example, we define a signal called products
, which represents an array of products and their prices. The computed signal, totalValue
is then created, which uses the reduce()
method to calculate the sum of all product prices in the array.
We can easily calculate the total value of the products by using computed signals, regardless of changes in individual product prices.
Overall, computed signals offer a declarative and effective method of handling derived values. By avoiding pointless calculations, their hastily evaluated and memoized nature ensures optimal performance. With computed signals, we can keep our application responsive and effective while carrying out complicated computations, filtering data, or producing derived values from other signals.
3. Effects: Reacting to Signal Changes
Effects are methods that take place whenever the values of one or more signals change. Effects offer a practical way to respond to changes in the signal and carry out additional tasks or actions in response to those changes. Let's investigate effects more thoroughly with the aid of some code examples.
We employ the effect()
function and supply a callback function as its argument to produce an effect. The action that needs to be taken whenever the dependent signals change is represented by this callback function. Let's use a real-world example to demonstrate how effects work.
// Create a signal for user authentication status
isAuthenticated = signal(false);
// Create an effect to perform actions based on authentication status
// Place in ngOninit
ngOnInit(): void {
effect(() => {
if (this.isAuthenticated()) {
console.log('User is authenticated. Redirecting to dashboard...');
// Code to redirect to the dashboard can be added here
} else {
console.log('User is not authenticated. Redirecting to login page...');
// Code to redirect to the login page can be added here
}
});
// Simulate authentication status change
this.isAuthenticated.set(true);
}
In this illustration, the authentication status of a user is represented by a signal called isAuthenticated
. The effect is triggered when the isAuthenticated
signal changes and, depending on the authentication status, takes particular actions.
We determine the value of isAuthenticated
inside the callback function for the effect. We log a message to the console and redirect to the dashboard page if it is true, indicating that the user has been authenticated. We log a different message and direct the user to the login page, however, if the isAuthenticated
value is false, indicating that the user is not authenticated.
The use of effects to respond to changes in authentication status and initiate corresponding actions in our Angular application is illustrated by the example given here. It demonstrates the adaptability of effects in handling a variety of scenarios, like user authentication, and lets us include custom logic to meet the requirements of our application.
Conclusion
In this article, we looked at how Angular Signals play an important role in state management and rendering optimization in Angular applications. Developers gain precise control over state changes by using Signals, resulting in improved performance and reactivity. These Signals, which include writable signals, computed signals, and effects, provide benefits such as granular state tracking, simplified updates, automatic dependency tracking, and computed values that are evaluated lazily. Using Angular Signals allows developers to build stable, feature-rich web applications that provide a consistent user experience. Using Signals in Angular development projects allows us to realize the full potential of state management and rendering optimization, improving application performance and maintainability.
Top comments (2)
Love love love signals! It's a game changer
Yessss!! Glad you do Lars