Skip to content

Signals in Angular: Deep Dive

Signals were introduced in Angular 16 (as a developer preview) and became stable in Angular 17+. They represent a fundamental shift in Angular’s reactivity model, offering a simpler way to manage state and reactivity in applications.

Key Signal Types Introduced

1. Writable Signals (signal())

The basic building block for reactive state:

import { signal } from '@angular/core';

count = signal(0); // Initial value

2. Computed Signals (computed())

Derived values that automatically update:

doubleCount = computed(() => this.count() * 2);

3. Effects (effect())

For side effects when signals change:

effect(() => {console.log('Count changed:', this.count());});

4. Input Signals (input())

(Introduced in Angular 17+)

name = input.required<string>(); // Required input
count = input(0); // Optional with default

5. Model Signals (model())

(Introduced in Angular 17+ for two-way binding)

value = model('default');

How Signals Work

Reactivity Graph

Signals create a dependency graph:

  1. When you read a signal (signal()), it tracks the consumer
  2. When the signal value changes, it notifies all dependents
  3. Computed values and effects re-evaluate automatically

Change Detection Mechanism

  1. Granular Updates: Only components that use changed signals are marked for update
  2. Glitch-Free: Angular ensures no intermediate states are shown
  3. No Zone.js Required: Signals can work without zone.js (future direction)

Performance Characteristics

  • Lazy Evaluation: Computed values only recalculate when needed
  • Memoization: Same signal values don’t trigger recomputations
  • Cleanup: Automatic dependency cleanup when contexts are destroyed

Core API Details

Signal Operations

const count = signal(0);

// Set value directly
count.set(5);

// Update based on previous value
count.update(v => v + 1);

// Mutate object/array contentsconst items = signal([{id: 1}]);
items.mutate(arr => arr.push({id: 2}));

Computed Deep Dive

const a = signal(1);const b = signal(2);

// Only recomputes when a or b changesconst sum = computed(() => a() + b());

// Computed signals are lazily evaluatedconsole.log(sum()); // First read triggers computation

Effect Behavior

effect(() => {// Runs immediately and whenever count changesconsole.log('Current count:', count());}, { allowSignalWrites: true }); // Optionally allow writing signals

Integration with Angular Features

Template Binding

<p>{{ count() }}</p><button (click)="count.update(c => c + 1)">Increment</button>

Component Communication

// Parent component
items = signal([...]);

// Child component@Input({ required: true }) items!: Signal<Item[]>;

RxJS Interop

import { toSignal, toObservable } from '@angular/core/rxjs-interop';

// Observable → Signal
data$ = this.http.get('/api/data');
data = toSignal(this.data$, { initialValue: [] });

// Signal → Observable
count$ = toObservable(this.count);

Advanced Patterns

Signal-Based Services

@Injectable({ providedIn: 'root' })export class CartService {private items = signal<CartItem[]>([]);
  totalItems = computed(() => this.items().length);
  
  addItem(item: CartItem) {this.items.update(items => [...items, item]);}}

Signal Queries (Angular 17+)

// Replaces ViewChild with signals
divRef = viewChild<ElementRef>('myDiv');
divRefs = viewChildren<ElementRef>('items');

Migration Considerations

  1. Gradual Adoption: Can be introduced alongside existing RxJS code
  2. Performance Benefits: Especially noticeable in large applications
  3. Future-Proofing: Signals are the foundation for Angular’s future reactivity

Signals represent Angular’s evolution toward simpler reactivity while maintaining performance. They work alongside (not replace) RxJS, with each having their optimal use cases.

Leave a Reply

Discover more from Sowft | Transforming Ideas into Digital Success

Subscribe now to keep reading and get access to the full archive.

Continue reading