Angular Signals: A Complete Guide

When Angular 17 came with the Angular signals, many developers were excited about it. I, for one, was excited about it.

Signals are reactive in nature and has this producer-consumer design. Signal came with a user-friendly interface, enabling developers to easily modify data to the framework. Its most exciting feature is its ability to enhance the efficiency of detecting changes and refreshing content, as well as addressing complexities that were previously challenging.

This blog post is to help you understand everything about the Angular signal and how to effectively make use of it.

For a constant stream of Angular content and news, consider installing the daily.dev browser extension. It delivers up-to-date news directly to your new tab, curated by the Angular core team and talented developers worldwide.

Understanding Angular Signals

With signals, Angular found a way, for our codes, to tell other codes that something has changed in the data.

  • In Angular, signals are a specific type of observable designed to optimize change detection for asynchronous data.

Now you might be tempted to ask, Signals and Observables, are they the same?

What we should ask is: what problems are signals and observables designed to address? The response is straightforward:

  • To perform tasks that occur independently of each other.
  • Observables have emitters that emit values, similar to signal towers broadcasting messages.
  • Observables function as dynamic streams of events in an application, encapsulating user interactions, data from APIs, or events based on time.

Understanding how Signals Work?

  • Angular.com describes Signals as wrappers. It is explained that signals wrap around a value. To simply put, it is like an eggshell that holds the egg yolk wrapped in it.
  • Angular signals wrap around a value (holds a value) and then notifies the user of any changes. Then to read the value from a signal you need a special function called a getter.
  • There are two types of signals: Writable Signals and Computed Signals (read-only):

import { Component, signal } from ā€˜@angular/coreā€™;

@Component({

selector: ā€˜app-signal-exampleā€™,

template: `

<div>

<p>Current Value: {{ mySignalValue() }}</p>

<button (click)=ā€setNewValue()ā€>Set New Value</button>

<button (click)=ā€updateValue()ā€>Update Value</button>

</div>

`,

})

export class SignalExampleComponent {

mySignal = signal({ foo: ā€˜barā€™ });

setNewValue() {

this.mySignal.set({ foo: ā€˜bar1ā€™ });

}

updateValue() {

const currentValue = this.mySignal();

this.mySignal.set({ ā€¦currentValue, foo: currentValue.foo + ā€˜1ā€™ });

}

}

Computed Signals

  • Computed signals are derived from other signals using a derivation function. They allow you to create dynamic values based on existing signals.
  • When a signal that a computed signal depends on (e.g., `count`) updates, the computed signal (e.g., `doubleCount`) is automatically recalculated.
  • Computed signals follow a lazy evaluation approach. The derivation function is executed only when the computed signalā€™s value is accessed for the first time. This avoids unnecessary computations until needed.
  • Unlike writable signals (which can be directly assigned values), computed signals cannot be assigned values directly. Attempting to set a value for a computed signal will result in a compilation error.

import { Component, signal, computed } from ā€˜@angular/coreā€™;

@Component({

selector: ā€˜app-computed-exampleā€™,

template: `

<div>

<p>Count: {{ countValue() }}</p>

<p>Double Count: {{ doubleCountValue() }}</p>

</div>

`,

})

export class ComputedExampleComponent {

// Create a writable signal for ā€˜countā€™

count = signal<number>(0);

// Create a computed signal (derived from ā€˜countā€™) for ā€˜doubleCountā€™

doubleCountValue = computed(() => {

return this.count() * 2

});

}

Pitfalls when Creating Computed Signals

When working with computed signals in Angular, it is important to be cautious about how dependencies are recognized.

  • Angular only identifies a signal dependency when it is explicitly used in the computation process of another signal.
  • The pitfall arises when a signal indirectly contributes to the final value of another signal, but the dependency is not explicitly stated in the computation function. In such cases, Angular may fail to register it as a dependency, resulting in updates to the ā€œhiddenā€ dependency not triggering updates in the computed signal.
  • For example, if `SignalA` contributes to the calculation of `ComputedSignalB`, but the relationship is not explicitly declared in the computation function of `ComputedSignalB`, any changes to `SignalA` may not propagate to `ComputedSignalB`. This can lead to unexpected behavior and incorrect computations.
  • To mitigate this issue, it is important to ensure that all relevant dependencies are explicitly declared within the computation functions. By doing so, Angular can accurately identify the relationships and maintain the expected reactivity in your application.

Dependency In Signals

In Angular, signal dependency refers to the relationship between two signals where one signal (the dependent signal) relies on the current value of another signal (the dependency signal) to calculate its own value.

This dependency allows for automatic updates in the dependent signal whenever the dependency signal changes, simplifying the management of interrelated data within an Angular application.

This allows for efficient communication and data flow between different parts of your application (components, services, directives).

import { Component, signal } from ā€˜@angular/coreā€™;

// Define the OrderStatus type (you can adjust this based on your actual requirements)

type OrderStatus = ā€˜placedā€™ | ā€˜cookingā€™ | ā€˜deliveredā€™;

@Component({

selector: ā€˜app-computed-exampleā€™,

template: `

<div>

<p>Order Status: {{ orderStatusValue() }}</p>

<p>Food Preparation Status: {{ prepareFoodValue() }}</p>

</div>

`,

})

export class ComputedExampleComponent {

// Create a writable signal for order status

orderStatus = signal<OrderStatus>(ā€˜placedā€™);

// Create a computed signal (derived from order status) for food preparation

prepareFoodValue = computed(() => {

return status === ā€˜placedā€™ ? ā€˜preparingā€™ : ā€˜idleā€™

});

constructor() {

// Update order status triggers recomputation of prepareFoodValue

this.orderStatus.set(ā€˜cookingā€™);

}

}

Here, `prepareFoodSignal` depends on the value of `orderStatusSignal`. When `orderStatusSignal` is updated, `prepareFoodSignal` is automatically recomputed to reflect the change.

Dynamic Additions and Removal of Dependency

import { Component, signal } from ā€˜@angular/coreā€™;

@Component({

selector: ā€˜app-profileā€™,

template: `

<div>

<p>User Logged In: {{ userLoggedIn() }}</p>

<p>Show Profile: {{ showProfileValue() }}</p>

</div>

`,

})

export class ProfileComponent {

// Create a writable signal for user login state

userLoggedIn = signal<boolean>(false);

// Create a computed signal (derived from user login state) for showing profile

showProfileValue = computed(() => {

return loggedIn ? ā€˜Yesā€™ : ā€˜Noā€™

});

constructor() {

// Simulate user login (you can replace this with actual login logic)

this.userLoggedIn.set(true);

// When user logs in, add dependency on user data (simulated by userDataSignal)

computed(() => {

if (this.userLoggedIn()) {

// Add dependency

} else {

// Remove dependency }

})

}

}

// Define the UserData type (you can adjust this based on your actual requirements)

type UserData = {

id: number;

name: string;

email: string;

};

  • Weā€™ve created an Angular component called `ProfileComponent`.
  • The `userLoggedIn` signal represents the user login state (initialized with `false`).
  • The `showProfileValue` is a computed signal derived from the user login state. It displays ā€œYesā€ if the user is logged in and ā€œNoā€ otherwise.
  • In the constructor, we simulate user login by setting `userLoggedIn.set(true)`.
  • The example demonstrates how to dynamically add or remove dependencies based on conditions (though in this case, we donā€™t actually add or remove dependencies).

Working with Signals and Arrays/Object Values

// Signal for shopping cart items (array of strings)

const cartItemsSignal = signal<string[]>([]);

// Donā€™t mutate the array directly (bad practice)

cartItemsSignal()[0] = ā€˜applesā€™; // Avoid this!

// Add a new item using proper methods (good practice)

cartItemsSignal.update(items => [ā€¦items, ā€˜breadā€™]);

Here, we demonstrate avoiding direct mutation of the array within the signal and instead using the `update` method to add a new item while maintaining immutability.

Overriding Signals Equality Check for Complex Values

  • By default, Angular signals (RxJS observables) use the Object.is function for equality checks.
  • Only objects with the exact same reference trigger change detection.
  • Modifying properties within an existing object wonā€™t be considered a change, as the object reference remains the same.
  • To handle complex objects (custom data structures), create a custom equality function.
  • This function should perform deep comparison to detect meaningful changes within nested properties or arrays.

// app.component.ts

import { Component } from ā€˜@angular/coreā€™;

@Component({

selector: ā€˜app-rootā€™,

template: `

<div>

<p>Objects are equal: {{ areObjectsEqual }}</p>

</div>

`,

})

export class AppComponent {

myObject = signal({

name: ā€œNnamdiā€

}, {

equals: (a, b) => a.name === b.name

})

}

Here, see that we override the equality check of the signal by providing ours in the equals property in the object passed as a second parameter to the signal function.

Our custom equality function is checking for equality in the name property of the myObject value.

Detecting Signal Changes with the effect() API

The `effect()` API allows you to react to changes in a signal. It takes a callback function that executes whenever the signalā€™s value updates.

const productPriceSignal = signal<number>(10);

effect(() => {

console.log(ā€˜Product price changed to:ā€™, productPriceSignal());

});

productPriceSignal.set(20); // Triggers the effect callback

This example logs the new product price whenever the `productPriceSignal` is updated.

Summary

Weā€™ve taken a deep dive into Angular Signals, a revolutionary concept that injects responsiveness into your applications. Gone are the days of plain data containers; Signals act as intelligent messengers, efficiently notifying Angular whenever information changes.

We explored two key Signal types:

  • Writable Signals: These are signals that can be updated directly, ideal for scenarios where you need to assign a value and proceed.
  • Computed Signals: These are signals that depend on other signals. When a dependency changes, the computed signal reevaluates its value, causing a cascade effect that updates anything that relies on it.

Building reactive apps isnā€™t without its challenges. We tackled potential pitfalls like handling conditional logic and dynamically adding or removing dependencies. By staying vigilant, you can ensure your signals flow smoothly and avoid unexpected behavior.

Signals and arrays/objects go hand-in-hand. We emphasized the importance of treating them with care. Techniques like .set() and .update() ensure you modify data in a predictable way, keeping your signals happy. For complex objects, we even touched on customizing the equality check for precise change detection.

Finally, we introduced the effect() API, a powerful tool that allows you to react to signal changes. Think of it as a detective constantly on the lookout, ready to spring into action whenever a signal value shifts.

This comprehensive guide equips you to transform your appā€™s performance. By embracing Angular Signals, youā€™ll unlock a world of:

  • Clear dependencies and well-defined communication channels lead to cleaner code.
  • Signals are lightning-fast at reacting to changes, keeping your UI up-to-date and dynamic.
  • DOM updates become laser-focused, minimizing unnecessary re-renders and keeping your app smooth and efficient.

--

--

Chidume Nnamdi šŸ”„šŸ’»šŸŽµšŸŽ®

JS | Blockchain dev | Author of ā€œUnderstanding JavaScriptā€ and ā€œArray Methods in JavaScriptā€ - https://app.gumroad.com/chidumennamdi šŸ“•