Overview
Signals are a reactive primitive that provide fine-grained reactivity for managing state in Angular applications. They enable automatic dependency tracking and efficient change detection.
Core Concepts
Writable Signals
Create signals using the signal() function to hold reactive values.
import { Component , signal } from '@angular/core' ;
@ Component ({
selector: 'app-counter' ,
template: `
<div class="counter">
<h2>Count: {{ count() }}</h2>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
// Create a writable signal with initial value
count = signal ( 0 );
increment () : void {
// Update signal value
this . count . update ( value => value + 1 );
}
decrement () : void {
this . count . update ( value => value - 1 );
}
reset () : void {
// Set absolute value
this . count . set ( 0 );
}
}
Reading Signal Values
Access signal values by calling them as functions.
import { Component , signal } from '@angular/core' ;
@ Component ({
selector: 'app-user-profile' ,
template: `
<div>
<h2>{{ username() }}</h2>
<p>Score: {{ score() }}</p>
</div>
`
})
export class UserProfileComponent {
username = signal ( 'John Doe' );
score = signal ( 0 );
constructor () {
// Read signal values in TypeScript
console . log ( 'Current username:' , this . username ());
console . log ( 'Current score:' , this . score ());
}
}
Computed Signals
Create derived values that automatically update when dependencies change.
import { Component , signal , computed } from '@angular/core' ;
interface CartItem {
id : number ;
name : string ;
price : number ;
quantity : number ;
}
@ Component ({
selector: 'app-shopping-cart' ,
template: `
<div class="cart">
<h2>Shopping Cart</h2>
<div *ngFor="let item of items()">
<span>{{ item.name }} x {{ item.quantity }}</span>
<span> \$ {{ item.price * item.quantity }}</span>
</div>
<hr>
<p>Total Items: {{ totalItems() }}</p>
<p>Subtotal: \$ {{ subtotal() }}</p>
<p>Tax (8%): \$ {{ tax() }}</p>
<p><strong>Total: \$ {{ total() }}</strong></p>
</div>
`
})
export class ShoppingCartComponent {
items = signal < CartItem []>([
{ id: 1 , name: 'Laptop' , price: 999.99 , quantity: 1 },
{ id: 2 , name: 'Mouse' , price: 29.99 , quantity: 2 }
]);
// Computed signals automatically track dependencies
totalItems = computed (() => {
return this . items (). reduce (( sum , item ) => sum + item . quantity , 0 );
});
subtotal = computed (() => {
return this . items (). reduce (
( sum , item ) => sum + ( item . price * item . quantity ),
0
);
});
tax = computed (() => this . subtotal () * 0.08 );
total = computed (() => this . subtotal () + this . tax ());
addItem ( item : CartItem ) : void {
this . items . update ( current => [ ... current , item ]);
}
updateQuantity ( id : number , quantity : number ) : void {
this . items . update ( current =>
current . map ( item =>
item . id === id ? { ... item , quantity } : item
)
);
}
}
Computed signals are read-only and automatically recompute only when their dependencies change, making them highly efficient.
Effects
Execute side effects when signals change using the effect() function.
import { Component , signal , effect } from '@angular/core' ;
import { AnalyticsService } from './analytics.service' ;
@ Component ({
selector: 'app-search' ,
template: `
<input
[value]="searchQuery()"
(input)="searchQuery.set($any($event.target).value)"
placeholder="Search..."
>
<p>Searching for: {{ searchQuery() }}</p>
`
})
export class SearchComponent {
searchQuery = signal ( '' );
constructor ( private analytics : AnalyticsService ) {
// Effect runs whenever searchQuery changes
effect (() => {
const query = this . searchQuery ();
if ( query . length > 2 ) {
console . log ( 'Searching for:' , query );
this . analytics . trackSearch ( query );
}
});
}
}
Effect Cleanup
Register cleanup functions to handle resource disposal.
import { Component , signal , effect } from '@angular/core' ;
@ Component ({
selector: 'app-auto-save' ,
template: `
<textarea
[value]="content()"
(input)="content.set($any($event.target).value)"
></textarea>
<p>{{ saveStatus() }}</p>
`
})
export class AutoSaveComponent {
content = signal ( '' );
saveStatus = signal ( 'All changes saved' );
constructor () {
effect (( onCleanup ) => {
const currentContent = this . content ();
this . saveStatus . set ( 'Saving...' );
// Set up auto-save timer
const timerId = setTimeout (() => {
this . saveToServer ( currentContent );
this . saveStatus . set ( 'All changes saved' );
}, 1000 );
// Cleanup function cancels pending save
onCleanup (() => {
clearTimeout ( timerId );
});
});
}
private saveToServer ( content : string ) : void {
console . log ( 'Saving:' , content );
}
}
Advanced Patterns
Signal-based State Management
import { Injectable , signal , computed } from '@angular/core' ;
export interface Todo {
id : number ;
title : string ;
completed : boolean ;
createdAt : Date ;
}
export type TodoFilter = 'all' | 'active' | 'completed' ;
@ Injectable ({ providedIn: 'root' })
export class TodoStore {
// Private state
private todos = signal < Todo []>([]);
private filter = signal < TodoFilter >( 'all' );
// Public computed selectors
readonly allTodos = this . todos . asReadonly ();
readonly filteredTodos = computed (() => {
const todos = this . todos ();
const currentFilter = this . filter ();
switch ( currentFilter ) {
case 'active' :
return todos . filter ( t => ! t . completed );
case 'completed' :
return todos . filter ( t => t . completed );
default :
return todos ;
}
});
readonly stats = computed (() => {
const todos = this . todos ();
return {
total: todos . length ,
active: todos . filter ( t => ! t . completed ). length ,
completed: todos . filter ( t => t . completed ). length
};
});
readonly currentFilter = this . filter . asReadonly ();
// Actions
addTodo ( title : string ) : void {
const newTodo : Todo = {
id: Date . now (),
title ,
completed: false ,
createdAt: new Date ()
};
this . todos . update ( todos => [ ... todos , newTodo ]);
}
toggleTodo ( id : number ) : void {
this . todos . update ( todos =>
todos . map ( todo =>
todo . id === id ? { ... todo , completed: ! todo . completed } : todo
)
);
}
removeTodo ( id : number ) : void {
this . todos . update ( todos => todos . filter ( t => t . id !== id ));
}
setFilter ( filter : TodoFilter ) : void {
this . filter . set ( filter );
}
clearCompleted () : void {
this . todos . update ( todos => todos . filter ( t => ! t . completed ));
}
}
Using the Store
import { Component } from '@angular/core' ;
import { TodoStore , TodoFilter } from './todo.store' ;
@ Component ({
selector: 'app-todos' ,
template: `
<div class="todo-app">
<input
#input
(keyup.enter)="addTodo(input.value); input.value = ''"
placeholder="What needs to be done?"
>
<div class="filters">
<button (click)="setFilter('all')">All ({{ store.stats().total }})</button>
<button (click)="setFilter('active')">Active ({{ store.stats().active }})</button>
<button (click)="setFilter('completed')">Completed ({{ store.stats().completed }})</button>
</div>
<ul>
<li *ngFor="let todo of store.filteredTodos()">
<input
type="checkbox"
[checked]="todo.completed"
(change)="store.toggleTodo(todo.id)"
>
<span [class.completed]="todo.completed">{{ todo.title }}</span>
<button (click)="store.removeTodo(todo.id)">Delete</button>
</li>
</ul>
<button
*ngIf="store.stats().completed > 0"
(click)="store.clearCompleted()"
>
Clear Completed
</button>
</div>
`
})
export class TodosComponent {
constructor ( public store : TodoStore ) {}
addTodo ( title : string ) : void {
if ( title . trim ()) {
this . store . addTodo ( title . trim ());
}
}
setFilter ( filter : TodoFilter ) : void {
this . store . setFilter ( filter );
}
}
RxJS Interop
Convert between signals and observables for integration with existing RxJS code.
import { Component , signal } from '@angular/core' ;
import { toObservable , toSignal } from '@angular/core/rxjs-interop' ;
import { debounceTime , switchMap } from 'rxjs/operators' ;
import { HttpClient } from '@angular/common/http' ;
interface SearchResult {
id : number ;
title : string ;
}
@ Component ({
selector: 'app-search-rxjs' ,
template: `
<input
[value]="searchTerm()"
(input)="searchTerm.set($any($event.target).value)"
>
<ul>
<li *ngFor="let result of results()">{{ result.title }}</li>
</ul>
<p *ngIf="isLoading()">Loading...</p>
`
})
export class SearchRxJSComponent {
searchTerm = signal ( '' );
// Convert signal to observable
private searchTerm$ = toObservable ( this . searchTerm );
// Process with RxJS operators
private results$ = this . searchTerm$ . pipe (
debounceTime ( 300 ),
switchMap ( term =>
term ? this . http . get < SearchResult []>( `/api/search?q= ${ term } ` ) : []
)
);
// Convert observable back to signal
results = toSignal ( this . results$ , { initialValue: [] });
isLoading = signal ( false );
constructor ( private http : HttpClient ) {}
}
Best Practices
Use Computed for Derived State Prefer computed() over manually updating multiple signals. It’s more efficient and easier to maintain.
Keep Signals Fine-Grained Split state into focused signals rather than one large object signal for better granularity.
Effects for Side Effects Only Use effect() for side effects, not for deriving state. Use computed() for derived values.
Immutable Updates Use immutable patterns with update() to ensure predictable state changes.
Don’t read signals inside effects without using them. This creates unnecessary dependencies and can cause performance issues.
Signals provide significant performance improvements:
Fine-grained updates : Only components reading changed signals are updated
Automatic dependency tracking : No manual subscription management
Efficient computation : Computed values only recalculate when dependencies change
Works with OnPush : Seamlessly integrates with OnPush change detection
Migration from RxJS
// Before: Using RxJS BehaviorSubject
import { BehaviorSubject } from 'rxjs' ;
export class OldComponent {
private countSubject = new BehaviorSubject ( 0 );
count$ = this . countSubject . asObservable ();
increment () : void {
this . countSubject . next ( this . countSubject . value + 1 );
}
}
// After: Using Signals
import { signal } from '@angular/core' ;
export class NewComponent {
count = signal ( 0 );
increment () : void {
this . count . update ( n => n + 1 );
}
}
Additional Resources