Signals
What are Signals?
Signals are a reactive primitive for managing state in Angular (v16+).
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
// Signal
count = signal(0);
// Computed signal
double = computed(() => this.count() * 2);
// Effect
constructor() {
effect(() => {
console.log('Count changed:', this.count());
});
}
increment() {
this.count.set(this.count() + 1);
// Or
this.count.update(value => value + 1);
}
}
Signal Operations
// Create signal
count = signal(0);
user = signal<User | null>(null);
items = signal<string[]>([]);
// Read value
console.log(this.count()); // 0
// Set value
this.count.set(10);
// Update value
this.count.update(value => value + 1);
// Mutate arrays/objects
this.items.mutate(arr => arr.push('new item'));
// Or better: immutable update
this.items.update(arr => [...arr, 'new item']);
Computed Signals
export class ShoppingCartComponent {
items = signal([
{ name: 'Item 1', price: 10, quantity: 2 },
{ name: 'Item 2', price: 20, quantity: 1 }
]);
// Automatically recalculated when items change
total = computed(() => {
return this.items().reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);
});
itemCount = computed(() => this.items().length);
isEmpty = computed(() => this.itemCount() === 0);
addItem(item: CartItem) {
this.items.update(items => [...items, item]);
}
}
Effects
export class UserComponent {
userId = signal(1);
constructor() {
// Runs when userId changes
effect(() => {
console.log('Loading user:', this.userId());
this.loadUser(this.userId());
});
// Cleanup
effect((onCleanup) => {
const subscription = this.someObservable$.subscribe();
onCleanup(() => {
subscription.unsubscribe();
});
});
}
}
Signal Inputs
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<h2>{{ user().name }}</h2>
<p>{{ user().email }}</p>
`
})
export class UserCardComponent {
// Signal input (Angular 17.1+)
user = input.required<User>();
// Optional with default
showAvatar = input(true);
// Derived value
displayName = computed(() => {
const user = this.user();
return `${user.firstName} ${user.lastName}`;
});
}
// Usage
<app-user-card [user]="currentUser" [showAvatar]="false" />
Signal vs Observable
// Observable (RxJS)
count$ = new BehaviorSubject(0);
this.count$.subscribe(value => {
console.log(value);
});
this.count$.next(1);
// Signal
count = signal(0);
effect(() => {
console.log(this.count());
});
this.count.set(1);
Interop with Observables
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
export class DataComponent {
userService = inject(UserService);
// Observable -> Signal
users = toSignal(this.userService.getUsers(), { initialValue: [] });
// Signal -> Observable
searchTerm = signal('');
searchTerm$ = toObservable(this.searchTerm);
constructor() {
this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.userService.search(term))
).subscribe(results => {
// Handle results
});
}
}
Complex State with Signals
interface TodoState {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
}
export class TodoService {
private state = signal<TodoState>({
todos: [],
filter: 'all'
});
// Selectors
todos = computed(() => this.state().todos);
filter = computed(() => this.state().filter);
filteredTodos = computed(() => {
const todos = this.todos();
const filter = this.filter();
switch (filter) {
case 'active':
return todos.filter(t => !t.completed);
case 'completed':
return todos.filter(t => t.completed);
default:
return todos;
}
});
// Actions
addTodo(title: string) {
this.state.update(state => ({
...state,
todos: [...state.todos, { id: Date.now(), title, completed: false }]
}));
}
setFilter(filter: TodoState['filter']) {
this.state.update(state => ({ ...state, filter }));
}
toggleTodo(id: number) {
this.state.update(state => ({
...state,
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
}));
}
}
Best Practices
- Use signals for local component state
- Use computed for derived values
- Use effects sparingly (side effects only)
- Prefer immutable updates over
mutate() - Use toSignal to bridge observables
- Signals enable OnPush by default
- Simpler than RxJS for simple state