Angular Signals
  • Angular Development
  • Angular Signals
  • Reactive Programming
  • web development

Angular Signals: A Comprehensive Introduction

Angular signals in Angular 17 and 18 for improved performance and state management. Learn benefits, workings, and see practical examples.

   

Introduction

Angular, a platform for building mobile and desktop web applications, constantly evolves to provide developers with more efficient tools and techniques. One such recent addition is Angular signals. This article aims to shed light on what Angular signals are, their benefits, how they work, why they are used, the advantages they bring to modern Angular development, and some practical use cases with examples to make the concept easy to grasp.

What are Angular Signals?

Angular Signals, introduced in Angular 17(and further refined in Angular 18), represent a paradigm shift in how we manage state and handle data flow in Angular applications. They offer a more efficient, reactive, and developer-friendly approach compared to traditional change detection mechanisms.

Understanding Angular Signals

At its core, a signal is a reactive wrapper around a value. It notifies dependent components or functions whenever its value changes. This granular control over data updates is a key factor in the performance improvements offered by signals.

How Signals Work?

Angular signals work by establishing a reactive connection between the state and the components that depend on that state. When the state changes, the signals ensure that all dependent components are notified and updated accordingly.

  • Signals are reactive wrappers around values.
  • When a signal’s value changes, dependent components or functions are notified.
  • Signals are accessed using a getter function, which allows Angular to track dependencies for optimized updates.
  • They can be writable or read-only.
  • Signals are created with an initial value.
  • Values are read by calling the signal as a function.
  • Values are modified using the set or update methods.
  • Angular automatically tracks dependencies, updating only affected components when a signal changes.

 

Example:

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

@Component({
  selector: 'app-counter',
  template: `
    <button (click)="increment()">Increment</button>
    <p>Count: {{ count() }}</p>
  `
})
export class CounterComponent {
// define a signal
  count = signal(0);

  increment() {
    this.count.set(this.count() + 1);
  }
}

In this example, the count signal is defined with an initial value of 0. The CounterComponent uses this signal, and the increment method updates the signal’s value. The component template automatically reflects the updated count value.

Why Use Signals in Angular?

1. Improved Performance

  • Granular change detection: Signals allow Angular to precisely track which parts of the application depend on specific data. This means only components that rely on changed data are updated, significantly boosting performance compared to traditional zone-based change detection.
  • Reduced overhead: By eliminating the need for unnecessary checks, signals contribute to a more efficient application.

2. Enhanced Developer Experience

  • Simpler state management: Signals offer a straightforward way to manage application state, making code more readable and maintainable.
  • Reactive updates: Changes to signal values automatically trigger updates in dependent components, simplifying the development process.
  • Better error handling: Signals provide more informative error messages, aiding in debugging.

Benefits of Using Signals

  • Improved Performance: Signals enable granular change detection, updating only components affected by data changes, leading to significant performance gains, especially in complex applications.
  • Enhanced Developer Experience: Signals simplify state management, making code more readable and maintainable. The reactive nature of signals reduces boilerplate code and improves developer productivity.
  • Better Error Handling: Signals provide more informative error messages, aiding in debugging and troubleshooting.
  • Standalone Components: Signals are compatible with standalone components, allowing for more flexible application architecture.

Use Cases of Angular Signals

1. Let’s Build a Todo App Using Angular Signals

Understanding the Goal

We’re going to create a simple application that allows users to:

  • Add new todo items to a list
  • View the list of todo items
  • Remove items from todo list

We’ll use Angular Signals to manage the list of todos and keep the UI updated automatically when changes occur.

Breaking Down the Code 

Import Necessary Module:

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

Create a Signal for Todos:

todos = Signal<string[]>([]);

Here, we create a signal named todos. A signal is like a container for data that can change. The Signal<string[]>([]) part tells Angular that this signal will hold an array of strings (which represent our todo items) and initially, it’s empty.

 

Create a Component:

@Component({
  selector: 'app-todo-list',
  template: `
   <div>
  <ul>
    <li *ngFor="let todo of filteredTodos()">
      {{ todo }}
      <button (click)="removeTodo(todo)">Remove</button>
    </li>
  </ul>
  <input #newTodo type="text">
  <button (click)="addTodo(newTodo.value)">Add Todo</button>
</div>
  `
})
export class TodoListComponent {
 todos = signal<string[]>([]);
  filter = signal('');

  filteredTodos = computed(() => this.todos().filter(todo => todo.includes(this.filter())));

  addTodo(todo: string) {
    this.todos.set([...this.todos(), todo]);
  }

  removeTodo(todo: string) {
    this.todos.set(this.todos().filter(item => item !== todo));
  }

}

This code creates a component called TodoListComponent. Components are building blocks of Angular applications that define a part of the user interface.

  • Template: This part defines the HTML structure of the component
    • It displays a list of todos using *ngFor to iterate over the todos.value array.
    • It has an input field to enter new todo items.
    • It has a button to add new todos.
  • Component Logic:
    • todos = signal<string[])>([]);: This line assigns the todos signal to a property in the component for easier access.
    • addTodo(todo: string): This function is called when the “Add Todo” button is clicked. It adds the new todo item to the todos array using the spread operator (…) to create a new array without modifying the original one.
    • removeTodo(todo: string): This function is called when the “Remove Todo” button is clicked. It removes the selected todo item to the todos array.

How It Works

  1. The todos signal holds an array of todo items.
  2. The component displays the todo items from the todos signal using *ngFor.
  3. When the user enters a new todo and clicks “Add Todo”, the addTodo function is called.
  4. The addTodo function adds the new todo to the todos signal.
  5. Angular automatically updates the UI to display the newly added todo item.
  6. When the user clicks “Remove” button, the removeTodo function is called.
  7. The removeTodo function remove the selected item from Todo array.
  8. Angular automatically updates the UI to display updated Todo items.

 

All the pieces aligned:

import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-todo-list',
  template: `
    <div>
      <ul>
        <li *ngFor="let todo of filteredTodos()">
          {{ todo }}
          <button (click)="removeTodo(todo)">Remove</button>
        </li>
      </ul>
      <input #newTodo type="text">
      <button (click)="addTodo(newTodo.value)">Add Todo</button>
    </div>
  `
})
export class TodoListComponent {
  todos = signal<string[]>([]);
  filter = signal('');

  filteredTodos = computed(() => this.todos().filter(todo => todo.includes(this.filter())));
  addTodo(todo: string) {
    this.todos.set([...this.todos(), todo]);
  }

  removeTodo(todo: string) {
    this.todos.set(this.todos().filter(item => item !== todo));
  }
}

2. Building a Shopping Cart with Angular Signals

This code demonstrates a shopping cart using Angular Signals. Let’s break down the functionalities:

  1. Define Shopping Cart Structure:
interface Product {

  name: string;

  price: number;

}

We create an interface called Product to define the structure of items in the cart. This ensures consistency and clarity in representing product data.

  1. Create a Signal for Cart Items:

 

We create a signal named cartItems to store the list of products in the cart. It holds an array of Product objects.

cartItems = signal<Product[]>([]);

We create a signal named cartItems to store the list of products in the cart. It holds an array of Product objects.

  1. Display Cart Items:.
<ul>

  <li *ngFor="let item of cartItems()">

    {{ item.name }} - {{ item.price | currency }}

    <button (click)="removeItem(item)">Remove</button>

  </li>

</ul>
  • The *ngFor directive iterates over the cartItems() array, displaying each product’s name and price with a currency format.
  • A “Remove” button is included for each item, triggering the removeItem function.
  1. Calculate Total Price:
total = computed(() => this.cartItems().reduce((sum, item) => sum + item.price, 0));

We define a computed property named total. It utilizes the reduce method on the cartItems() array to calculate the total price by summing up the price of each product. This ensures the total automatically updates whenever the cart contents change.

  1. Add Items:
addItem() {

  const newItem = { name: 'Item ' + (this.cartItems().length + 1), price: Math.random() * 100 };

  this.cartItems.set([...this.cartItems(), newItem]);

}

The addItem function generates a new Product object with a random price and adds it to the cartItems signal using the set method. The spread operator (…) ensures we create a new array instead of directly modifying the existing one.

  1. Remove Items:
removeItem(item: Product) {

  this.cartItems.set(this.cartItems().filter(existingItem => existingItem !== item));

}

The removeItem function takes a Product object as input. It filters the cartItems array to exclude the selected item using the filter method. The filtered array is then set back to the cartItems signal using the set method.

  • The *ngFor directive iterates over the cartItems() array, displaying each product’s name and price with a currency format.

  • A “Remove” button is included for each item, triggering the removeItem function.

How it works

  1. Signal for Cart Items: The cartItems signal holds an array of Product objects, representing the items in the cart.
  2. Computed Total: The total property is a computed value that dynamically calculates the total price based on the current items in the cart. Whenever the cartItems signal changes, the total is automatically recalculated.
  3. Adding Items: The addItem function creates a new product object and adds it to the cartItems signal using the set method, ensuring the cart updates accordingly.
  4. Removing Items: The removeItem function filters out the selected item from the cartItems array and updates the signal with the new array.

All the pieces aligned:

import { Component, signal, computed } from '@angular/core';

interface Product {
  name: string;
  price: number;
}

@Component({
  selector: 'app-shopping-cart',
  template: `
    <div>
      <ul>
        <li *ngFor="let item of cartItems()">
          {{ item.name }} - {{ item.price | currency }}
          <button (click)="removeItem(item)">Remove</button>
        </li>
      </ul>
      <p>Total: {{ total | currency }}</p>
      <button (click)="addItem()">Add Item (Random)</button>
    </div>
  `
})
export class ShoppingCartComponent {
 // define a signal for the shopping cart
cartItems = signal<Product[]>([]);

  total:any = computed(() => this.cartItems().reduce((sum, item) => sum + item.price, 0));

  addItem() {
    const newItem = { name: 'Item ' + (this.cartItems().length + 1), price: Math.random() * 100 };
    this.cartItems.set([...this.cartItems(), newItem]);
  }

  removeItem(item: Product) {
    this.cartItems.set(this.cartItems().filter(existingItem => existingItem !== item));
  }
}

3. Building a Basic Authentication System with Angular Signals

Understanding the Goal

  1. Define User Interface:

interface User {

  name: string;

  loggedIn: boolean;

}

We define a User interface to represent the structure of a user object. It includes the user’s name and a flag indicating whether they are logged in.

  1. Create a Signal for User:

user = signal<User | null>(null);

We create a signal named user to store the user information. It can hold either a User object or null to represent the logged-out state. Initially, it’s set to null.

 

  1. Create the Component:

@Component({
  selector: 'app-user-auth',
  template: `
    <div *ngIf="user()">
      <p>Welcome, {{ user()?.name }}</p>
      <button (click)="logout()">Logout</button>
    </div>
    <div *ngIf="!user()">
      <button (click)="login()">Login</button>
    </div>
  `
})
export class UserAuthComponent {
  user = user;

  login() {
    this.user.set({ name: 'User', loggedIn: true });
  }

  logout() {
    this.user.set(null);
  }
}
  • The UserAuthComponent displays different content based on the user’s login status.
  • If the user is logged in (user() is truthy), it displays a welcome message and a logout button.
  • If the user is not logged in (user() is falsy), it displays a login button.
  • The login function sets the user signal to a new User object with the user’s name and loggedIn flag set to true.
  • The logout function sets the user signal to null, indicating the user is logged out.

 

How It Works

  1. The user signal stores the current user information or null if the user is not logged in.
  2. The component displays different content based on the user signal’s value.
  3. When the user clicks the “Login” button, the login function updates the user signal with user information.
  4. When the user clicks the “Logout” button, the logout function sets the user signal to null.

All the pieces aligned:

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

interface User {
  name: string;
  loggedIn: boolean;
}

@Component({
  selector: 'app-user-auth',
  template: `
    <div *ngIf="user()">
      <p>Welcome, {{ user()?.name }}</p>
      <button (click)="logout()">Logout</button>
    </div>
    <div *ngIf="!user()">
      <button (click)="login()">Login</button>
    </div>
  `
})
export class UserAuthComponent {
// define a signal for the user
user = signal<User | null>(null);

  login() {
    this.user.set({ name: 'User', loggedIn: true });
  }

  logout() {
    this.user.set(null);
  }
}

Conclusion

Angular signals offer a powerful and efficient way to handle state management in Angular applications. By providing a reactive and declarative approach, signals simplify state management, improve performance, and enhance code maintainability. Whether building a simple counter application, a todo list, a shopping cart, or handling user authentication, signals can significantly streamline the development process. Embracing Angular signals can lead to more predictable, maintainable, and performant applications, making them a valuable addition to any developer’s toolkit.

Feel free to reach out if you need further assistance or if you’re looking to Hire Angular developer for your project!