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
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);
}
}
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]
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
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
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);
});
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
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);
}
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);
});
});
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);
});
});
Best Practices
- Use Signals for Local State: Avoid using signals for complex, app-wide state.
-
Keep Computed Signals Pure: Avoid side effects in
computed
functions. - Leverage Effects for Side Effects: Separate side effects from state updates.
- 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)