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 : `
-
@for (todo of filteredTodos(); track todo.id) {
- {{ todo.text }} }
{{ remaining() }} remaining
` , } ) 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 .