Signals
What are Signals?
Section titled “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
Section titled “Signal Operations”// Create signalcount = signal(0);user = signal<User | null>(null);items = signal<string[]>([]);
// Read valueconsole.log(this.count()); // 0
// Set valuethis.count.set(10);
// Update valuethis.count.update(value => value + 1);
// Mutate arrays/objectsthis.items.mutate(arr => arr.push('new item'));
// Or better: immutable updatethis.items.update(arr => [...arr, 'new item']);Computed Signals
Section titled “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
Section titled “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
Section titled “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
Section titled “Signal vs Observable”// Observable (RxJS)count$ = new BehaviorSubject(0);
this.count$.subscribe(value => { console.log(value);});
this.count$.next(1);
// Signalcount = signal(0);
effect(() => { console.log(this.count());});
this.count.set(1);Interop with Observables
Section titled “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
Section titled “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
Section titled “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

