DEV Community

Cover image for Angular Signals: A Quick Guide
hassantayyab
hassantayyab

Posted on

Angular Signals: A Quick Guide

Introduction

Angular signals represent a paradigm shift in state management and reactivity, offering developers a streamlined, declarative approach. From foundational principles to advanced concepts like testing and RxJS integration, this guide will provide you with everything you need to master Angular signals, supported by detailed examples and practical tips.


Core Concepts

What Are Angular Signals?

Signals in Angular are reactive primitives that track and react to data changes automatically. They reduce boilerplate code and enhance performance by enabling fine-grained reactivity.

Core Features

  • Declarative Reactivity: Signals explicitly declare their dependencies.
  • Optimized Change Detection: Updates propagate efficiently, minimizing unnecessary recalculations.
  • Debugging Support: Angular provides tools to inspect signal dependencies and values.

Usage of Signals

Creating Signals

Define a signal using the signal function. Signals hold a reactive value that can be read or updated:

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

// Define a signal with an initial value of 0
const counter = signal(0);

// Read the current value of the signal
console.log(counter()); // Outputs: 0

// Update the signal value
counter.set(1);
console.log(counter()); // Outputs: 1

Enter fullscreen mode Exit fullscreen mode

Using Signals in Components

Signals integrate seamlessly into Angular templates:

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

@Component({
  selector: 'app-counter',
  template: `
    <p>Counter: {{ counter() }}</p>
    <button (click)="increment()">Increment</button>
  `,
})
export class CounterComponent {
  // Define a signal to manage the counter state
  counter = signal(0);

  // Method to increment the counter
  increment() {
    this.counter.update(value => value + 1);
  }
}

Enter fullscreen mode Exit fullscreen mode

Updating Signals

Signals offer various methods to update their values:

  • set(value): Directly assign a value.
  • update(fn): Update based on the current value.
  • mutate(fn): Efficiently modify complex objects or arrays.
import { signal } from '@angular/core';

// Signal managing a list of numbers
const list = signal([1, 2, 3]);

// Mutate the signal by adding an element to the array
list.mutate(arr => arr.push(4));
console.log(list()); // Outputs: [1, 2, 3, 4]

Enter fullscreen mode Exit fullscreen mode

Advanced Topics

Derived Signals

Derived signals use the computed function to reactively calculate values based on other signals:

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

// Define a base signal
const base = signal(5);

// Define a derived signal that depends on the base signal
const double = computed(() => base() * 2);

console.log(double()); // Outputs: 10

// Update the base signal value
base.set(10);
console.log(double()); // Outputs: 20

Enter fullscreen mode Exit fullscreen mode

Effects

Effects allow you to execute side effects whenever a signal’s value changes:

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

// Define a signal for a count value
const count = signal(0);

// Effect to log the count value whenever it changes
effect(() => {
  console.log(`Count is now: ${count()}`);
});

// Update the signal value
count.set(5); // Logs: Count is now: 5

Enter fullscreen mode Exit fullscreen mode

Dependency Tracking

Signals automatically track their dependencies, ensuring updates propagate only when necessary. This reduces unnecessary computations and boosts performance.

Signal Lifecycles

Signals clean up automatically when a component is destroyed. For manual resource management, use the cleanup callback:

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

// Define an effect with a cleanup callback
effect(() => {
  const interval = setInterval(() => console.log('Running...'), 1000);

  // Cleanup the interval when the effect is disposed
  return () => clearInterval(interval);
});

Enter fullscreen mode Exit fullscreen mode

Combining Signals with RxJS

Integrate signals with RxJS workflows using toObservable or RxJS operators:

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

// Define a signal
const counter = signal(0);

// Convert the signal to an observable
const counter$ = toObservable(counter);

// Subscribe to the observable
counter$.subscribe(value => console.log(value));

// Update the signal value
counter.set(10); // Logs: 10

Enter fullscreen mode Exit fullscreen mode

Asynchronous Signals

Handle asynchronous workflows by combining signals with promises or RxJS:

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

// Function to fetch data and update a signal
async function fetchData() {
  const dataSignal = signal(null);
  const data = await fetch('https://api.example.com/data').then(res => res.json());
  dataSignal.set(data);
}

Enter fullscreen mode Exit fullscreen mode

Signal Debugging Tools

Angular provides built-in tools for debugging signals, helping you inspect signal states, dependencies, and updates in real-time.


Testing Angular Signals

Unit Testing Signals

Use Angular’s testing utilities to verify signal behavior:

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

describe('Signal Tests', () => {
  it('should update signal value', () => {
    const count = signal(0);

    // Set a new value for the signal
    count.set(5);

    // Assert the updated value
    expect(count()).toBe(5);
  });
});

Enter fullscreen mode Exit fullscreen mode

Testing Effects

Mock dependencies and track side effects:

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

describe('Effect Tests', () => {
  it('should log changes', () => {
    const spy = jest.fn();
    const count = signal(0);

    // Define an effect that logs changes
    effect(() => spy(count()));

    // Update the signal and assert the effect
    count.set(5);
    expect(spy).toHaveBeenCalledWith(5);
  });
});

Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Use Signals for Local State: Avoid using signals for complex, app-wide state.
  2. Keep Computed Signals Pure: Avoid side effects in computed functions.
  3. Leverage Effects for Side Effects: Separate side effects from state updates.
  4. Integrate with RxJS: Use signals for lightweight reactivity and RxJS for complex asynchronous data streams.

Cheat Sheet

Signal Basics

  • Create a signal: const mySignal = signal(initialValue);
  • Read a signal: mySignal()
  • Update a signal: mySignal.set(newValue);
  • Modify a signal: mySignal.update(value => value + 1);
  • Mutate objects/arrays: mySignal.mutate(obj => { obj.key = value; });

Derived Signals

  • Create a derived signal: const derived = computed(() => mySignal() * 2);

Effects

  • Run side effects: effect(() => console.log(mySignal()));
  • Cleanup effects: return () => cleanupLogic();

RxJS Integration

  • Convert to observable: const obs$ = toObservable(mySignal);

Testing Signals

  • Assert signal value: expect(mySignal()).toBe(expectedValue);
  • Test effects: Use spies or mocks to track calls.

Conclusion

Mastering Angular signals unlocks the full potential of Angular’s reactivity system, offering a lightweight, declarative alternative to traditional state management. By understanding core concepts, advanced workflows, and testing strategies, you can build highly performant and maintainable applications.

Angular signals are a game-changer—dive in and transform your development workflow today!

Top comments (0)