Angular's change detection and reactivity system efficiently manages DOM updates and provides reactive programming primitives including signals, computed values, and effects for state management.
Core change detection APIs for manually controlling when Angular checks for changes.
/**
* Reference to the change detector for a component
*/
abstract class ChangeDetectorRef {
/**
* Mark component and ancestors as needing change detection
*/
abstract markForCheck(): void;
/**
* Detach change detector from change detection tree
*/
abstract detach(): void;
/**
* Manually trigger change detection for this component
*/
abstract detectChanges(): void;
/**
* Check that no changes have occurred (dev mode only)
*/
abstract checkNoChanges(): void;
/**
* Reattach previously detached change detector
*/
abstract reattach(): void;
}
/**
* Change detection strategies
*/
enum ChangeDetectionStrategy {
/** Use OnPush change detection - only check when inputs change */
OnPush = 0,
/** Use default change detection - check on every cycle */
Default = 1
}Zone.js integration for automatic change detection triggering.
/**
* Service for executing work inside or outside Angular's zone
*/
class NgZone {
/**
* Execute function inside Angular zone (triggers change detection)
* @param fn - Function to execute
* @returns Function result
*/
run<T>(fn: (...args: any[]) => T): T;
/**
* Execute function outside Angular zone (no change detection)
* @param fn - Function to execute
* @returns Function result
*/
runOutsideAngular<T>(fn: (...args: any[]) => T): T;
/**
* Execute function when Angular zone is stable
* @param fn - Function to execute
* @returns Function result
*/
runTask<T>(fn: (...args: any[]) => T): T;
/** Observable that emits when zone becomes stable */
readonly onStable: EventEmitter<any>;
/** Observable that emits when zone becomes unstable */
readonly onUnstable: EventEmitter<any>;
/** Observable that emits when zone encounters an error */
readonly onError: EventEmitter<any>;
/** Whether currently in Angular zone */
readonly isStable: boolean;
}
/**
* Provider for zone-based change detection
* @param options - Zone configuration options
* @returns Environment providers for zone-based change detection
*/
function provideZoneChangeDetection(options?: NgZoneOptions): EnvironmentProviders;
/**
* Provider for zoneless change detection (experimental)
* @returns Environment providers for zoneless change detection
*/
function provideZonelessChangeDetection(): EnvironmentProviders;
/**
* Configuration options for NgZone
*/
interface NgZoneOptions {
/** Enable long stack trace (for debugging) */
enableLongStackTrace?: boolean;
/** Should coalesce event change detection */
shouldCoalesceEventChangeDetection?: boolean;
/** Should coalesce run change detection */
shouldCoalesceRunChangeDetection?: boolean;
}Core reactive programming primitives using signals for state management.
/**
* Create a writable signal with initial value
* @param initialValue - Initial value for the signal
* @param options - Signal creation options
* @returns Writable signal instance
*/
function signal<T>(initialValue: T, options?: CreateSignalOptions<T>): WritableSignal<T>;
/**
* Create a computed signal derived from other signals
* @param computation - Function that computes the value
* @param options - Computed signal options
* @returns Readonly computed signal
*/
function computed<T>(computation: () => T, options?: CreateComputedOptions<T>): Signal<T>;
/**
* Create reactive effect that runs when signals change
* @param effectFn - Effect function to execute
* @param options - Effect creation options
* @returns Effect reference for cleanup
*/
function effect(
effectFn: (onCleanup: EffectCleanupRegisterFn) => void,
options?: CreateEffectOptions
): EffectRef;
/**
* Read signals without tracking dependencies
* @param fn - Function to execute without tracking
* @returns Function result
*/
function untracked<T>(fn: () => T): T;
/**
* Check if value is a signal
* @param value - Value to check
* @returns True if value is a signal
*/
function isSignal(value: unknown): value is Signal<unknown>;
/**
* Readonly signal interface
*/
interface Signal<T> {
/** Get current signal value */
(): T;
}
/**
* Writable signal interface
*/
interface WritableSignal<T> extends Signal<T> {
/** Set signal to new value */
set(value: T): void;
/** Update signal using current value */
update(updateFn: (value: T) => T): void;
/** Get readonly version of signal */
asReadonly(): Signal<T>;
}
/**
* Options for creating signals
*/
interface CreateSignalOptions<T> {
/** Custom equality function for change detection */
equal?: ValueEqualityFn<T>;
}
/**
* Options for creating computed signals
*/
interface CreateComputedOptions<T> {
/** Custom equality function for change detection */
equal?: ValueEqualityFn<T>;
}
/**
* Options for creating effects
*/
interface CreateEffectOptions {
/** Injector to use for effect context */
injector?: Injector;
/** Whether effect should run manually */
manualCleanup?: boolean;
/** Allow effect to write to signals */
allowSignalWrites?: boolean;
}
/**
* Reference to created effect
*/
interface EffectRef {
/** Destroy effect and cleanup resources */
destroy(): void;
}
/**
* Function type for value equality comparison
*/
type ValueEqualityFn<T> = (a: T, b: T) => boolean;
/**
* Function type for registering effect cleanup
*/
type EffectCleanupRegisterFn = (cleanupFn: EffectCleanupFn) => void;
/**
* Function type for effect cleanup
*/
type EffectCleanupFn = () => void;Effects that run after Angular's render phase.
/**
* Create effect that runs after every render
* @param effectFn - Effect function to execute
* @param options - After render options
* @returns Effect reference
*/
function afterEveryRender(
effectFn: (phase: AfterRenderPhase) => void,
options?: AfterRenderOptions
): EffectRef;
/**
* Create effect that runs after next render only
* @param effectFn - Effect function to execute
* @param options - After render options
* @returns Effect reference
*/
function afterNextRender(
effectFn: (phase: AfterRenderPhase) => void,
options?: AfterRenderOptions
): EffectRef;
/**
* Options for after render effects
*/
interface AfterRenderOptions {
/** Injector to use for effect context */
injector?: Injector;
/** Render phase to execute in */
phase?: AfterRenderPhase;
}
/**
* Phases of Angular's render cycle
*/
enum AfterRenderPhase {
/** Early read phase */
EarlyRead = 0,
/** Write phase */
Write = 1,
/** Mixed read/write phase */
MixedReadWrite = 2,
/** Read phase */
Read = 3
}
/**
* Reference to after render effect
*/
interface AfterRenderRef {
/** Destroy effect */
destroy(): void;
}System for detecting changes in collections and objects.
/**
* Registry for iterable differ factories
*/
class IterableDiffers {
/**
* Create differ for iterable collection
* @param iterable - Collection to create differ for
* @param trackByFn - Optional track by function
* @returns Iterable differ instance
*/
find(iterable: any): IterableDifferFactory;
/**
* Create IterableDiffers with custom factories
* @param factories - Array of differ factories
* @param parent - Optional parent differs
* @returns IterableDiffers instance
*/
static create(factories: IterableDifferFactory[], parent?: IterableDiffers): IterableDiffers;
/** Extend with additional factories */
static extend(factories: IterableDifferFactory[]): StaticProvider;
}
/**
* Differ for iterable collections
*/
interface IterableDiffer<V> {
/** Calculate differences from previous state */
diff(object: NgIterable<V> | undefined | null): IterableChanges<V> | null;
}
/**
* Factory for creating iterable differs
*/
interface IterableDifferFactory {
/** Check if factory supports the collection type */
supports(objects: any): boolean;
/** Create differ instance */
create<V>(trackByFn?: TrackByFunction<V>): IterableDiffer<V>;
}
/**
* Registry for key-value differ factories
*/
class KeyValueDiffers {
/**
* Create differ for key-value object
* @param kv - Object to create differ for
* @returns Key-value differ factory
*/
find(kv: any): KeyValueDifferFactory;
/** Create KeyValueDiffers with custom factories */
static create<S>(factories: KeyValueDifferFactory[], parent?: KeyValueDiffers): KeyValueDiffers;
/** Extend with additional factories */
static extend<S>(factories: KeyValueDifferFactory[]): StaticProvider;
}
/**
* Differ for key-value objects
*/
interface KeyValueDiffer<K, V> {
/** Calculate differences from previous state */
diff(object: Map<K, V> | {[key: string]: V} | undefined | null): KeyValueChanges<K, V> | null;
}
/**
* Factory for creating key-value differs
*/
interface KeyValueDifferFactory {
/** Check if factory supports the object type */
supports(objects: any): boolean;
/** Create differ instance */
create<K, V>(): KeyValueDiffer<K, V>;
}
/**
* Function type for tracking items in collections
*/
type TrackByFunction<T> = (index: number, item: T) => any;
/**
* Type for iterable collections
*/
type NgIterable<T> = Array<T> | Iterable<T>;Service for tracking asynchronous operations and application stability.
/**
* Service for tracking pending asynchronous tasks
*/
class PendingTasks {
/**
* Add a pending task
* @returns Task ID for later removal
*/
add(): number;
/**
* Remove a pending task
* @param taskId - ID of task to remove
*/
remove(taskId: number): void;
/** Whether there are pending tasks */
hasPendingTasks: Signal<boolean>;
}import { Component, ChangeDetectorRef, ChangeDetectionStrategy, inject } from '@angular/core';
@Component({
selector: 'app-manual-detection',
template: `
<div>
<h3>Manual Change Detection</h3>
<p>Count: {{count}}</p>
<p>Last updated: {{lastUpdated}}</p>
<button (click)="increment()">Increment</button>
<button (click)="updateOutsideZone()">Update Outside Zone</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualDetectionComponent {
private cdr = inject(ChangeDetectorRef);
count = 0;
lastUpdated = new Date();
increment(): void {
this.count++;
this.lastUpdated = new Date();
// OnPush strategy requires manual marking
this.cdr.markForCheck();
}
updateOutsideZone(): void {
// Simulate async operation outside Angular zone
setTimeout(() => {
this.count += 10;
this.lastUpdated = new Date();
// Must manually trigger change detection
this.cdr.detectChanges();
}, 1000);
}
}import { Component, NgZone, inject } from '@angular/core';
@Component({
selector: 'app-zone-example',
template: `
<div>
<h3>Zone Management</h3>
<p>Counter: {{counter}}</p>
<button (click)="startInsideZone()">Start Inside Zone</button>
<button (click)="startOutsideZone()">Start Outside Zone</button>
</div>
`
})
export class ZoneExampleComponent {
private ngZone = inject(NgZone);
counter = 0;
startInsideZone(): void {
// This will trigger change detection automatically
setInterval(() => {
this.counter++;
}, 1000);
}
startOutsideZone(): void {
// Run outside Angular zone to avoid triggering change detection
this.ngZone.runOutsideAngular(() => {
setInterval(() => {
// Update happens outside zone
this.counter++;
// Manually trigger change detection when needed
this.ngZone.run(() => {
console.log('Manual change detection triggered');
});
}, 1000);
});
}
ngOnInit(): void {
// Listen for zone stability
this.ngZone.onStable.subscribe(() => {
console.log('Zone is stable');
});
this.ngZone.onUnstable.subscribe(() => {
console.log('Zone became unstable');
});
}
}import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-signals-example',
template: `
<div>
<h3>Reactive Signals</h3>
<p>First Name: <input [(ngModel)]="firstName" (input)="updateFirstName($event)"></p>
<p>Last Name: <input [(ngModel)]="lastName" (input)="updateLastName($event)"></p>
<p>Full Name: {{fullName()}}</p>
<p>Initials: {{initials()}}</p>
<p>Character Count: {{characterCount()}}</p>
<button (click)="reset()">Reset</button>
</div>
`
})
export class SignalsExampleComponent {
// Writable signals
firstName = signal('John');
lastName = signal('Doe');
// Computed signals (automatically update when dependencies change)
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
initials = computed(() => {
const first = this.firstName().charAt(0).toUpperCase();
const last = this.lastName().charAt(0).toUpperCase();
return `${first}.${last}.`;
});
characterCount = computed(() => this.fullName().length);
constructor() {
// Effects run when signals change
effect(() => {
console.log(`Full name changed to: ${this.fullName()}`);
});
// Effect with cleanup
effect((onCleanup) => {
const timer = setInterval(() => {
console.log(`Current name: ${this.fullName()}`);
}, 5000);
onCleanup(() => {
clearInterval(timer);
});
});
}
updateFirstName(event: Event): void {
const target = event.target as HTMLInputElement;
this.firstName.set(target.value);
}
updateLastName(event: Event): void {
const target = event.target as HTMLInputElement;
this.lastName.set(target.value);
}
reset(): void {
this.firstName.set('John');
this.lastName.set('Doe');
}
}import { Component, ElementRef, afterNextRender, afterEveryRender, viewChild } from '@angular/core';
@Component({
selector: 'app-after-render',
template: `
<div>
<canvas #canvas width="400" height="200"></canvas>
<button (click)="redraw()">Redraw</button>
</div>
`
})
export class AfterRenderComponent {
canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');
private frameCount = 0;
constructor() {
// Run once after next render
afterNextRender(() => {
console.log('Component rendered for the first time');
this.initializeCanvas();
});
// Run after every render
afterEveryRender(() => {
this.frameCount++;
console.log(`Render frame: ${this.frameCount}`);
});
// After render with specific phase
afterEveryRender(() => {
this.updateCanvasIfNeeded();
}, {
phase: AfterRenderPhase.Read
});
}
private initializeCanvas(): void {
const ctx = this.canvas().nativeElement.getContext('2d');
if (ctx) {
ctx.fillStyle = 'lightblue';
ctx.fillRect(0, 0, 400, 200);
}
}
private updateCanvasIfNeeded(): void {
// Safe to read DOM properties here
const canvas = this.canvas().nativeElement;
if (canvas.width !== canvas.offsetWidth) {
console.log('Canvas size changed, updating...');
}
}
redraw(): void {
const ctx = this.canvas().nativeElement.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, 400, 200);
ctx.fillStyle = `hsl(${Math.random() * 360}, 50%, 50%)`;
ctx.fillRect(0, 0, 400, 200);
}
}
}import { Component, TrackByFunction } from '@angular/core';
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: 'app-tracking-example',
template: `
<div>
<h3>Collection Tracking</h3>
<button (click)="addUser()">Add User</button>
<button (click)="shuffleUsers()">Shuffle</button>
<button (click)="updateUser()">Update First User</button>
<ul>
<li *ngFor="let user of users; trackBy: trackByUserId; index as i">
{{i}}: {{user.name}} ({{user.email}})
</li>
</ul>
</div>
`
})
export class TrackingExampleComponent {
users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
];
// TrackBy function for efficient list updates
trackByUserId: TrackByFunction<User> = (index: number, user: User) => user.id;
addUser(): void {
const newId = Math.max(...this.users.map(u => u.id)) + 1;
this.users.push({
id: newId,
name: `User ${newId}`,
email: `user${newId}@example.com`
});
}
shuffleUsers(): void {
this.users = this.users.sort(() => Math.random() - 0.5);
}
updateUser(): void {
if (this.users.length > 0) {
this.users[0] = {
...this.users[0],
name: `Updated ${Date.now()}`
};
}
}
}