Angular Signals
Signals are Angular's reactive primitive for state management. They provide synchronous, fine-grained reactivity.
Core Signal APIs
signal() - Writable State
import { signal } from '@angular/core';
// Create writable signal const count = signal(0);
// Read value console.log(count()); // 0
// Set new value count.set(5);
// Update based on current value count.update(c => c + 1);
// With explicit type const user = signal<User | null>(null); user.set({ id: 1, name: 'Alice' });
computed() - Derived State
import { signal, computed } from '@angular/core';
const firstName = signal('John'); const lastName = signal('Doe');
// Derived signal - automatically updates when dependencies change
const fullName = computed(() => ${firstName()} ${lastName()});
console.log(fullName()); // "John Doe" firstName.set('Jane'); console.log(fullName()); // "Jane Doe"
// Computed with complex logic const items = signal<Item[]>([]); const filter = signal('');
const filteredItems = computed(() => { const query = filter().toLowerCase(); return items().filter(item => item.name.toLowerCase().includes(query) ); });
const totalPrice = computed(() => filteredItems().reduce((sum, item) => sum + item.price, 0) );
linkedSignal() - Dependent State with Reset
import { signal, linkedSignal } from '@angular/core';
const options = signal(['A', 'B', 'C']);
// Resets to first option when options change const selected = linkedSignal(() => options()[0]);
console.log(selected()); // "A" selected.set('B'); // User selects B console.log(selected()); // "B" options.set(['X', 'Y']); // Options change console.log(selected()); // "X" - auto-reset to first
// With previous value access const items = signal<Item[]>([]);
const selectedItem = linkedSignal<Item[], Item | null>({ source: () => items(), computation: (newItems, previous) => { // Try to preserve selection if item still exists const prevItem = previous?.value; if (prevItem && newItems.some(i => i.id === prevItem.id)) { return prevItem; } return newItems[0] ?? null; }, });
effect() - Side Effects
import { signal, effect, inject, DestroyRef } from '@angular/core';
@Component({...}) export class Search { query = signal('');
constructor() { // Effect runs when query changes effect(() => { console.log('Search query:', this.query()); });
// Effect with cleanup
effect((onCleanup) => {
const timer = setInterval(() => {
console.log('Current query:', this.query());
}, 1000);
onCleanup(() => clearInterval(timer));
});
} }
Effect rules:
-
Run in injection context (constructor or with runInInjectionContext )
-
Automatically cleaned up when component destroys
Component State Pattern
@Component({ selector: 'app-todo-list', template: ` <input [value]="newTodo()" (input)="newTodo.set($any($event.target).value)" /> <button (click)="addTodo()" [disabled]="!canAdd()">Add</button>
<ul>
@for (todo of filteredTodos(); track todo.id) {
<li [class.done]="todo.done">
{{ todo.text }}
<button (click)="toggleTodo(todo.id)">Toggle</button>
</li>
}
</ul>
<p>{{ remaining() }} remaining</p>
`, }) export class TodoList { // State todos = signal<Todo[]>([]); newTodo = signal(''); filter = signal<'all' | 'active' | 'done'>('all');
// Derived state canAdd = computed(() => this.newTodo().trim().length > 0);
filteredTodos = computed(() => { const todos = this.todos(); switch (this.filter()) { case 'active': return todos.filter(t => !t.done); case 'done': return todos.filter(t => t.done); default: return todos; } });
remaining = computed(() => this.todos().filter(t => !t.done).length );
// Actions addTodo() { const text = this.newTodo().trim(); if (text) { this.todos.update(todos => [ ...todos, { id: crypto.randomUUID(), text, done: false } ]); this.newTodo.set(''); } }
toggleTodo(id: string) { this.todos.update(todos => todos.map(t => t.id === id ? { ...t, done: !t.done } : t) ); } }
RxJS Interop
toSignal() - Observable to Signal
import { toSignal } from '@angular/core/rxjs-interop'; import { interval } from 'rxjs';
@Component({...}) export class Timer { private http = inject(HttpClient);
// From observable - requires initial value or allowUndefined counter = toSignal(interval(1000), { initialValue: 0 });
// From HTTP - undefined until loaded users = toSignal(this.http.get<User[]>('/api/users'));
// With requireSync for synchronous observables (BehaviorSubject) private user$ = new BehaviorSubject<User | null>(null); currentUser = toSignal(this.user$, { requireSync: true }); }
toObservable() - Signal to Observable
import { toObservable } from '@angular/core/rxjs-interop'; import { switchMap, debounceTime } from 'rxjs';
@Component({...}) export class Search { query = signal('');
private http = inject(HttpClient);
// Convert signal to observable for RxJS operators
results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
switchMap(q => this.http.get<Result[]>(/api/search?q=${q}))
),
{ initialValue: [] }
);
}
Signal Equality
// Custom equality function const user = signal<User>( { id: 1, name: 'Alice' }, { equal: (a, b) => a.id === b.id } );
// Only triggers updates when ID changes user.set({ id: 1, name: 'Alice Updated' }); // No update user.set({ id: 2, name: 'Bob' }); // Triggers update
Untracked Reads
import { untracked } from '@angular/core';
const a = signal(1); const b = signal(2);
// Only depends on 'a', not 'b' const result = computed(() => { const aVal = a(); const bVal = untracked(() => b()); return aVal + bVal; });
Service State Pattern
@Injectable({ providedIn: 'root' }) export class Auth { // Private writable state private _user = signal<User | null>(null); private _loading = signal(false);
// Public read-only signals readonly user = this._user.asReadonly(); readonly loading = this._loading.asReadonly(); readonly isAuthenticated = computed(() => this._user() !== null);
private http = inject(HttpClient);
async login(credentials: Credentials): Promise<void> { this._loading.set(true); try { const user = await firstValueFrom( this.http.post<User>('/api/login', credentials) ); this._user.set(user); } finally { this._loading.set(false); } }
logout(): void { this._user.set(null); } }
For advanced patterns including resource(), see references/signal-patterns.md.