Understanding Angular Signals: A Comprehensive Guide

Katie Mikova / Wednesday, July 17, 2024

In the ever-evolving world of web development, Angular continues to blaze a trail by relentlessly enhancing the capabilities of Angular programmers. Known for its unwavering commitment to improvement, it once again pushes the boundaries of what's possible, and this time, it introduces an exciting new feature with the Angular v16 release: Angular Signals.

If you’re new to it or have already tried the developer preview but want to know more about how Signal works, this comprehensive guide will help you understand it.

Key topics to be explored:

What Are Angular Signals & Why Is It Important To Learn How To Use Them Right Now?

In essence, Angular Signals is a reactivity model that works as a zero-argument function [(() => T)] and returns a specific value when executed. How does it do that? There is one important thing here—the getter function. The call of the getter function returns the current value, and Angular recognizes and updates these reactive values automatically when their dependencies change.

Here is a diagram to help visualize this process:

Angular Signals diagram

Now, when you update your model, Angular automatically applies the changes to the corresponding view. This enables a seamless and synchronized model-view interaction, ultimately leading to a smoother UX.

Since Angular Signals are based on a pattern called the Observer Design Pattern, there is a Publisher that stores the value and a list of Subscribers who are interested in it. Once the value changes, they receive a notification. In other words, you indicate that only one thing has changed, and therefore, it needs to be updated. This eliminatels the need for Angular to check the entire component tree for changes. No overly complex operations here.

If there’s one major advantage of this feature, it is its ability to simplify updating state values and to optimize rendering performance. Using it, developers gain fine-grained control over state changes while benefitting from things like:

  • Automatic dependency tracking: Angular automatically recognizes and updates reactive values.
  • Simpler-to-handle state updates: Fine-grained control over state changes with minimal complexity.
  • Optimized performance: No need to check the entire component tree for changes, which enhances performance.
  • Computed values that are evaluated lazily.
  • Granular state tracking.
  • Possibility to implement Signals outside of components (working just fine with Dependency Injection).
  • Improved Angular Change Detection
  • Better app performance and maintainability.

Angular Signals vs RxJS

With all this, many consider Angular Signals as powerful as RxJS but with a simpler syntax. What does this mean for RxJS? Is it doomed? Not in my opinion. The new feature may be able to hide some of the RxJS complexities like manual subscription management and memory leak risks, but it won’t replace RxJS any time soon.

I’ve highlighted the importance of this new feature and defined it. Let’s move on to explaining how to create and how to use Signals in Angular.

Hands-On: Getting Started With Angular Signals

I will examine three primitives and show you how they work and when to use each one.

Prerequisites:

  • Previous experience with Angular.
  • Basic knowledge of the framework’s Reactive principles.
  • Angular 16 version installed or running.

First things first, this brand-new feature presents three reactive primitives: Writable Signals, Computed Signals, and Effects, to enable you to achieve reactivity in your Angular app.

  1. Using writable Signals

These are Signals that can be directly updated, but to have them, you must define them first. Here’s a technical demonstration:

Import { signal } from ‘@angular/core’;
const count = signal(0);
Count.set(count() +1);
  1. Using computed Signals

With Computed Signals, you get a declarative method for handling derived values. When there's a change in any of the Signals that a function depends on, the computed Signals recalculate and produce a read-only derived Signal. Here’s a technical demonstration:

import { computed, signal } from '@angular/core';
 const count = signal(0);
const doubled = computed(() => count() * 2);

  1. Using effects

Effects in Angular Signals will run its function in response to changes in the Signal. Inside the effect() you can modify the state imperatively, change the DOM manually, and run asynchronous code. Here’s a technical demonstration:

import { effect, signal } from '@angular/core';
const count = signal(0);
effect(() => {
  console.log('Count changed to:', count());
});
count.set(1);

Angular Signals Examples

Here's a practical example to see Angular Signals in action. Let's create a simple counter application using all three primitives:

import { Component } from '@angular/core';
import { signal, computed, effect } from '@angular/core';
 
@Component({
  selector: 'app-counter',
  template: `
    <div>
      <button (click)="increment()">Increment</button>
      <p>Count: {{ count() }}</p>
      <p>Doubled: {{ doubled() }}</p>
    </div>
  `
})
export class CounterComponent {
  count = signal(0);
  doubled = computed(() => this.count() * 2);
 
  constructor() {
    effect(() => {
      console.log('Count changed to:', this.count());
    });
  }
 
  increment() {
    this.count.set(this.count() + 1);
  }
}
 

Angular Signals and Ignite UI: What Do We Have Under The Sleeve?

Combining Angular Signals with Ignite UI for Angular can elevate your web applications to the next level. Here’s a quick look at how this powerful combo can work for you. Angular Signals simplify state management by tracking changes precisely, while Ignite UI provides robust, high-performance UI components. Together, they make your app faster and more responsive.

Now, let’s build a real-time data grid using Angular Signals and Ignite UI.

  1. Set up Signals - define Signals for your data and any filters

import { Component } from '@angular/core';
import { signal, computed, effect } from '@angular/core';
import { IgxGridComponent } from 'igniteui-angular';
 
@Component({
  selector: 'app-data-grid',
  template: `
    <igx-grid [data]="filteredData()">
      <!-- Define your grid columns here -->
    </igx-grid>
  `
})
export class DataGridComponent {
  data = signal([]);  // Signal to store our data
  filter = signal('');  // Signal for filtering the data
 
  // Computed signal to apply the filter to the data
  filteredData = computed(() =>
    this.data().filter(item => item.name.includes(this.filter()))
  );
 
  constructor() {
    // Effect to log changes for debugging
    effect(() => {
      console.log('Data or filter changed:', this.filteredData());
    });
  }
 
  // Method to fetch and update data
  fetchData(newData) {
    this.data.set(newData);
  }
}
 

  1. Update data: fetch new data and update the Signal

fetchData(newData) {
      this.data.set(newData);
    }
 

  1. Experience smooth updates

The grid updates in real time as data changes, thanks to the reactive power of Angular Signals and the efficient rendering of Ignite UI. But here is another example—without Angular Signals.

import { Component } from '@angular/core';
 
@Component({
  selector: 'app-data-grid',
  template: `
    <input [(ngModel)]="filter" (ngModelChange)="applyFilter()" placeholder="Filter" />
    <igx-grid [data]="filteredData">
      <!-- Define your grid columns here -->
    </igx-grid>
  `
})
export class DataGridComponent {
  data = [];  // Data array
  filteredData = [];  // Filtered data array
  filter = '';  // Filter string
 
  applyFilter() {
    this.filteredData = this.data.filter(item => item.name.includes(this.filter));
  }
 
  fetchData(newData) {
    this.data = newData;
    this.applyFilter();
  }
}
With:
import { Component } from '@angular/core';
import { signal, computed, effect } from '@angular/core';
 
@Component({
  selector: 'app-data-grid',
  template: `
    <input [value]="filter()" (input)="filter.set($event.target.value)" placeholder="Filter" />
    <igx-grid [data]="filteredData()">
      <!-- Define your grid columns here -->
    </igx-grid>
  `
})
export class DataGridComponent {
  data = signal([]);  // Signal to store data
  filter = signal('');  // Signal for filter
 
  // Computed signal to filter data
  filteredData = computed(() =>
    this.data().filter(item => item.name.includes(this.filter()))
  );
 
  constructor() {
    // Effect to log changes for debugging
    effect(() => {
      console.log('Data or filter changed:', this.filteredData());
    });
  }
 
   fetchData(newData) {
    this.data.set(newData);
  }
}

In Conclusion…

Here is a simplified explanation of why you would ever want to use Angular Signals: without Angular Signals, you manually update and manage the state and its effects. This often involves writing functions to update the state and trigger changes, making the code more complex and error-prone. With Angular Signals, state management becomes more declarative, efficient, and less error-prone, improving both performance and maintainability.

Ignite UI Angular