Angular Signals: Complete Guide to Reactive State Management
🔄 What is a Signal?
A signal is a variable + change notification - it's Angular's reactive primitive for state management.
import { signal } from '@angular/core';
// Think of a signal as a smart variable that notifies when it changes
const count = signal(0); // Initial value: 0
console.log(count()); // Read: 0
count.set(5); // Write: 5
console.log(count()); // Read: 5
// The magic: When count changes, Angular automatically knows to update the UI!
What Makes a Signal Special?
A signal is NOT just a regular variable:
// Regular variable - NO change notification
let regularCount = 0;
regularCount = 5; // Angular doesn't know this changed!
// Signal - WITH change notification
const signalCount = signal(0);
signalCount.set(5); // Angular automatically detects this change! ✨
Key Characteristics of Signals:
- ✅ Always has a value - no undefined states
- ✅ Synchronous - no async/await needed
- ✅ Reactive - automatically notifies dependencies
- ✅ Memoized - computed values are cached
- ✅ Type-safe - full TypeScript support
- ✅ Variable + Change Notification - the core concept!
🚀 Creating and Using Signals
Primitive Signals
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<h2>Count: {{ count() }}</h2>
<h3>Name: {{ name() }}</h3>
<h3>Is Active: {{ isActive() }}</h3>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="toggleActive()">Toggle</button>
</div>
`
})
export class CounterComponent {
// Primitive signals - each is a "variable + change notification"
count = signal(0); // number signal
name = signal('John'); // string signal
isActive = signal(false); // boolean signal
increment() {
this.count.set(this.count() + 1);
}
decrement() {
this.count.update(value => value - 1); // Alternative syntax
}
toggleActive() {
this.isActive.update(active => !active);
}
}
Array Signals
@Component({
selector: 'app-todo-list',
template: `
<div>
<h3>Todos ({{ todos().length }})</h3>
<ul>
<li *ngFor="let todo of todos()">
{{ todo.text }} - {{ todo.completed ? 'Done' : 'Pending' }}
</li>
</ul>
<button (click)="addTodo()">Add Todo</button>
<button (click)="clearCompleted()">Clear Completed</button>
</div>
`
})
export class TodoListComponent {
todos = signal<Todo[]>([
{ id: 1, text: 'Learn Angular Signals', completed: false },
{ id: 2, text: 'Build awesome apps', completed: false }
]);
addTodo() {
const newTodo = {
id: Date.now(),
text: `Todo ${this.todos().length + 1}`,
completed: false
};
// Using update() for arrays
this.todos.update(todos => [...todos, newTodo]);
}
clearCompleted() {
this.todos.update(todos => todos.filter(todo => !todo.completed));
}
toggleTodo(id: number) {
this.todos.update(todos =>
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}
}
interface Todo {
id: number;
text: string;
completed: boolean;
}
Object Signals
@Component({
selector: 'app-user-profile',
template: `
<div>
<h2>{{ user().name }}</h2>
<p>Email: {{ user().email }}</p>
<p>Age: {{ user().age }}</p>
<p>Address: {{ user().address.city }}, {{ user().address.country }}</p>
<button (click)="updateAge()">Update Age</button>
<button (click)="updateAddress()">Update Address</button>
</div>
`
})
export class UserProfileComponent {
user = signal<User>({
name: 'John Doe',
email: 'john@example.com',
age: 30,
address: {
city: 'New York',
country: 'USA'
}
});
updateAge() {
this.user.update(user => ({
...user,
age: user.age + 1
}));
}
updateAddress() {
this.user.update(user => ({
...user,
address: {
...user.address,
city: 'San Francisco'
}
}));
}
// Using set() to replace entire object
resetUser() {
this.user.set({
name: 'Jane Smith',
email: 'jane@example.com',
age: 25,
address: {
city: 'Boston',
country: 'USA'
}
});
}
}
interface User {
name: string;
email: string;
age: number;
address: {
city: string;
country: string;
};
}
📊 Signal Methods: set(), update()
set() - Replace Entire Value
const count = signal(0);
const user = signal({ name: 'John', age: 30 });
// set() replaces the entire value
count.set(10); // count: 0 → 10
user.set({ name: 'Jane', age: 25 }); // Completely new object
// With arrays
const items = signal(['a', 'b', 'c']);
items.set(['x', 'y', 'z']); // Completely new array
update() - Transform Current Value
const count = signal(0);
const user = signal({ name: 'John', age: 30 });
// update() receives current value and returns new value
count.update(current => current + 1); // 0 → 1
count.update(current => current * 2); // 1 → 2
// With objects (immutable updates)
user.update(current => ({
...current,
age: current.age + 1
}));
// With arrays
const items = signal(['a', 'b']);
items.update(current => [...current, 'c']); // Add item
items.update(current => current.filter(item => item !== 'a')); // Remove item
When to Use Which?
// Use set() when:
// - Replacing entire value
// - Value comes from external source
// - Resetting to default state
this.user.set(apiResponse.user);
this.todos.set([]);
this.loading.set(false);
// Use update() when:
// - Modifying existing value
// - Calculations based on current value
// - Immutable updates to objects/arrays
this.count.update(n => n + 1);
this.user.update(u => ({ ...u, lastLogin: Date.now() }));
this.items.update(arr => [...arr, newItem]);