Prerequisites
- Practical experience working with Angular
- Familiarity with Angular's reactive programming concepts
Why Signals Matter in Angular Apps
Angular Signals bring a distinctive set of benefits to state management and rendering performance compared to other reactive tools like RxJS. Their key strengths stem from computed signals that evaluate lazily and cache results, dependency tracking that happens automatically, straightforward state mutations, precise observation of state, and seamless cooperation with the change detection system. With Signals, developers gain the ability to manage state updates at a very precise level, making state handling both efficient and targeted.
Writable Signals simplify the process of changing state values, eliminating the need for complex operations. Automatic dependency tracking ensures that only the necessary updates are triggered, which improves rendering efficiency. Computed signals that defer evaluation and remember their output prevent unnecessary recalculations. And because Signals are wired into Angular's change detection, components using the OnPush strategy receive updates when they are truly required. The combination of these features makes Angular Signals a compelling choice for handling state and optimizing renders in Angular applications.
Defining Angular Signals
We have touched on what Angular Signals are, but it is worth taking a moment to clarify the underlying concept of a signal itself. Picture a room where the occupants are deaf and mute; the only way to communicate is through hand gestures. If we want them to rise, we lift our hands in a sweeping motion. They observe the gesture and stand up, shifting their state from seated to standing. That is a signal: a communication that prompts a change in state.
Angular Signals operate on the same principle. A variable is declared as a signal. When its value is modified, the signal detects the change and propagates it, updating the variable and notifying any interested parties. In essence, Angular signals are variables that hold a value and emit a notification whenever that value changes, creating a reactive flow within the application.
Creating and Working with Angular Signals
With a solid understanding of what signals are and why they matter, let's examine how to put them into practice within Angular applications. The Angular team shipped three reactive primitives as part of version 16:
Writable Signals
Computed Signals
Effects
These primitives form the foundation for leveraging signals to bring reactivity into Angular apps. Beyond just introducing them, we'll explore how to combine and apply these building blocks in practical scenarios.
A companion Stackblitz project for this article is available here to help solidify the concepts.
1. Writable Signals
Writable signals are the category of Angular signals whose values can be changed directly. They function as predefined signals. A signal needs to be created before it can be read, modified, or updated. Creating a writable signal is a straightforward process, as illustrated in the code snippet below.
// defination of a signal
our_first_signal = signal<number>(0);
This line of code shows the technical process of defining a signal. We declare a property named our_first_signal and assign it the result of the signal() function, which is provided by the Angular team. The function is invoked with an initial value of 0. This assignment transforms our_first_signal into a signal rather than a standard property.
Signals are essentially getter functions. Consequently, to access the value held by our signal, we invoke it as we would a regular function. The following code demonstrates how to retrieve a signal's value.
//reading the value of signal
ngOnInit(): void {
console.log("Our first signal:", this.our_first_signal())
// returns 0
}
Within this snippet, a console.log statement outputs the value of the signal, which we obtain by calling it.
We've covered defining a signal and reading its value. Now, let's tackle the more dynamic part: updating the signal. Suppose we want to change the value of our signal from 0 to 5. How can this be done? The Angular team provides the set() and update() methods for this purpose. The update() method alters the signal based on its existing value, while the set() method directly replaces the signal's value. The lines below illustrate how to use these methods effectively.
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
}
In the initial line, the set() method assigns a value of 8 to our_first_signal. Moving to the second line, the update() method is used to increment the signal's value by 2. This method accepts a callback function as an argument. The callback receives the current signal value, and its returned value becomes the new signal value. The third line logs the final value of our_first_signal to the console.
It's crucial to remember that signals can work with numbers, strings, arrays, and objects. When handling arrays or objects, you might need to change a single property while preserving the rest of the structure. The Angular team provides the mutate() method for this scenario, which alters the inner content of a signal value instead of replacing the value itself. For instance, if we have an array of objects with name and age properties and want to update only the age, we would rely on the mutate() method. The following code demonstrates its application.
// 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())
}
In the example above, we created a signal named persons and assigned it an array of objects. Using the mutate() method, we directly changed the age property of the first object to 21. Finally, we called the signal and printed it to the console. The resulting output is shown below.
The console confirms that the first object's age has been updated to 21. This is a result of the mutate() method's behavior: it accepts a callback function as an argument. This callback receives the current signal value as input and is expected to return the new value for the signal.
2. Computed Signals: Getting Dynamic Values
Computed signals serve as a formidable feature in Angular for deriving values based on other signals. These signals are evaluated lazily and their results are memoized, which ensures both efficiency and reactivity in how derived values are handled.
The computed() function is used to create a computed signal, taking a derivation function as its input. Let's delve into some code examples to see them in action.
// 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
}
The snippet begins by defining two basic signals, price and quantity, to track a product's cost and count.
Next, we create a computed signal called totalCost. Its derivation function calculates the total cost by multiplying the values from the price and quantity signals. Importantly, this calculation only runs when totalCost is first read or when its underlying dependencies (price or quantity) are updated.
Finally, we output the value of totalCost to the console. Given the initial values of price and quantity, this logs 50.
A key advantage of computed signals is their capacity to handle complex calculations and transformations. Consider a scenario where we have a signal holding an array of products, and we need to calculate the total value of all items in that array:
// 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
}
Here, we introduce a signal called products that holds an array of product objects along with their prices. We then define a computed signal, totalValue, which employs the reduce() method to sum up the prices of all products in the array.
Through computed signals, determining the aggregate value of the products becomes effortless, even as individual product prices fluctuate.
In summary, computed signals present a declarative and streamlined approach to managing derived values. Their lazy evaluation and memoization prevent unnecessary calculations, ensuring optimal performance. Whether we're performing intricate computations, filtering data, or generating values from other signals, computed signals help maintain a responsive and efficient application.
3. Effects: Reacting to Signal Changes
Effects are processes that execute whenever the value of one or more specified signals changes. They offer a straightforward mechanism for responding to signal updates and triggering associated tasks or actions based on those modifications. To better understand effects, let's look at some code examples.
To create an effect, we use the effect() function, passing a callback function as its argument. This callback represents the specific action that should be executed when the relevant signals change. We can use a common example to demonstrate the utility of effects.
// 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 case, a signal named isAuthenticated tracks a user's login state. When this signal changes, the effect activates and performs distinct actions based on whether the user is authenticated.
Inside the effect's callback, we inspect the value of isAuthenticated. If it evaluates to true, signifying the user has logged in, we log a success message and navigate them to the dashboard. Conversely, if the isAuthenticated value is false—indicating the user is logged out—we log a different message and redirect them to the login page.
This example highlights how effects can be used to react to authentication status changes and initiate the appropriate responses within an Angular application. It showcases the flexibility of effects in handling various situations, such as user authentication, and allows us to integrate custom logic tailored to our application's needs.
Wrapping Up: Signals and State Management
Throughout this discussion, we've explored the critical role Angular Signals play in managing state and enhancing rendering efficiency within Angular applications. By leveraging Signals, developers can achieve fine-grained control over state transitions, which directly translates to better performance and more responsive interactions. The core Signal types—writable, computed, and effects—each contribute unique advantages: they enable tracking at a granular level, make updates more straightforward, handle dependency tracing automatically, and compute derived values in a lazy fashion. Adopting Signals empowers developers to create reliable and feature-rich Angular applications that ensure a smooth and consistent user experience. By incorporating Signals into our Angular development workflows, we can unlock the complete potential of state management and rendering optimization, leading to stronger performance and more maintainable codebases.

