Return Blog
Angular

Signals in Angular: The Future of Change Detection

Sergio Rojas
Sergio Rojas
5 min read 16 May, 2024
Share
Signals in Angular: The Future of Change Detection
Summarize with AI:
Prompt copied! Paste it (Cmd/Ctrl+V) in the chat. Open AI

Angular’s Renaissance According to Sarah Drasner

Hello! Today we are going to explore an exciting innovation in Angular: Signals. Sarah Drasner, Director of Engineering at Google, has given us hints about how these new features are revitalizing Angular, making our applications faster and more responsive.

In this article, we explore how Signals were used in Angular projects, making them more efficient and engaging. It was an update that definitely turned the way we code upside down!

What are Signals?

Think of Signals as direct channels that allow specific parts of your application to respond to changes without affecting the rest. It’s like having private conversations at a loud party: only the people involved need to pay attention.

Change Detection Today: Zone.js

Traditionally, Angular has used Zone.js to manage component updates—a method that is effective but heavy. Zone.js monitors all activities across the application, which can slow down performance as the app grows. Additionally, it introduces several disadvantages:

  • Monkey Patching:

It modifies browser objects unpredictably, making error diagnostics complicated.

  • Overhead:

It adds around 100 KB, which is a considerable weight for lightweight web components.

  • Limitations with Async/Await:

This forces the Angular CLI to downlevel async and await into promises, even though modern browsers already support them natively.

  • Inefficient Checking:

Angular traverses the entire component tree to detect changes, which is inefficient when you only want to update specific components.

Signals solve these drawbacks by allowing Angular to update only the necessary parts more efficiently and with lower overhead.

How to Use Signals in Angular

If you are using Angular 16 or higher, you can already start experimenting with Signals. Here is a simple example of how to implement them:

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

const count = signal(0); // Initializes a signal with a value of 0
console.log('The counter is at: ' + count()); // Displays the initial value of the signal

- Updating a Signal

To change the value, you can use the .set() method, or the .update() method to modify the value based on its previous state.

count.set(3); // Sets the signal value to 3
count.update(value => value + 1); // Increments the signal value by 1

Computed Signals

Computed Signals observe other Signals and automatically update their value in response to changes. They are ideal for creating dynamic relationships within your application.

const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);

In this example, doubleCount depends on count. Every time count updates, doubleCount will automatically update as well. This update is lazy and memoized, meaning the derived value is not recalculated until it is actually needed, optimizing performance.

- Utilizing Computed Signals

Imagine you want to display a message only when it is relevant:

const showCounter = signal(false);
const count = signal(0);
const conditionalCounter = computed(() => {
  if (showCounter()) {
    return `The counter is at ${count()}.`;
  } else {
    return 'Nothing to see here!';
  }
});

This example illustrates the flexibility and efficiency of Computed Signals. When the showCounter Signal is false, the count Signal has absolutely no influence because its value isn’t even read.

This means that if you change the value of count, it will not affect or trigger any update in conditionalCounter. Thus, conditionalCounter is only recalculated when truly necessary, avoiding wasteful work and boosting application performance.

Using Signals in Forms

Although ngModel does not natively support Signals for two-way binding yet, you can manage it manually.

In this example form, we have two input fields that users use to enter their origin and destination cities for a flight search. These fields are managed by two Signals named from and to. Here is a closer look at the example:

<form>
  <input [ngModel]="from()" (ngModelChange)="from.set($event)">
  <input [ngModel]="to()" (ngModelChange)="to.set($event)">
  <button (click)="search()">Search Flights</button>
</form>

For each input field, we bind the value of the Signal using [ngModel]="from()", which returns the current value.

When the input field value changes, (ngModelChange)="from.set($event)" updates the Signal with the new value using .set(), keeping the form reactive to user changes despite the current limitations of ngModel with Signals.

Signals in Effects

When working with Signals inside effects, we must be extra careful to avoid creating unintended loops.

Imagine an effect that reads Signal a and updates Signal b, while another effect does the exact opposite. This could quickly lead to an infinite loop of updates, something we want to avoid at all costs.

- Restrictions on Writing to Signals inside Effects

By default, Angular does not allow you to write directly to a Signal inside an effect to precisely prevent these loops. Let’s look at an example where we try to synchronize two Signals:

effect(() => {
  // Attempting to write to a signal here will throw an error
  this.to.set(this.from());
});

This attempt will result in a specific error:

ERROR Error: NG0600: Writing to signals is not allowed in a computed or 
an effect by default. Use allowSignalWrites in the CreateEffectOptions to 
enable this inside effects.

For situations where you genuinely need to write to a Signal inside an effect, you can use the allowSignalWrites option to explicitly allow it:

effect(() => {
  this.to.set(this.from());
}, { allowSignalWrites: true });

*- Asynchronous Effects and Indirect Usage

Effects in Angular do not have to be instantaneous. In fact, we can make them operate in the background, handling tasks that don’t require immediate action, such as loading data asynchronously.

effect(async () => {
  const flights = await this.flightService.findAsPromise(this.from(), this.to());
  this.flights.set(flights);
});

This code showcases how an asynchronous effect can improve efficiency. Using await, the effect waits for an operation to complete—like a flight search—before updating a Signal.

Angular handles these wait times effectively, allowing other parts of the application to remain fast and responsive. Furthermore, Signals can be used more subtly to control updates without overloading the system, which is ideal when minor actions shouldn’t trigger massive processes.

- The Art of Debouncing with Signals

Imagine you are developing a search bar with autocomplete features. If every single letter typed by the user triggered a full search request, the system would quickly overload. To prevent this, we can use Signals indirectly to implement debouncing, optimizing the process without sacrificing functionality.

effect(() => {
  this.search();
});

This Effect pauses before executing the search, letting the user finish typing. This reduces API requests and keeps the system running smoothly.

Wrapping up

And there you have it! Signals transform how we handle changes in Angular, making our applications more efficient and reactive.

If you are using Angular 16 or higher, experiment with this innovative tool and take your projects to the next level. Discover the power of Signals and revitalize your applications today!