Angular Signals: Reactive State Made Simple

Angular 16 introduced Signals — a reactive primitive that gives you fine-grained change detection without Zone.js overhead or RxJS boilerplate. Here's everything you need to get started.

The Problem Signals Solve

Angular's traditional change detection runs top-down after every async event, checking the entire component tree. This is expensive at scale. RxJS handles async streams elegantly, but brings considerable boilerplate for simple synchronous state like a counter, a toggle, or a form value.

Signals are Angular's answer: lightweight, synchronous, and automatically tracked. When a signal's value changes, only the parts of the template that depend on it re-render — nothing else.

The Core API

signal() — writable state

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

const count = signal(0);

// Read — call it like a function
console.log(count()); // 0

// Write
count.set(5);
count.update(val => val + 1); // 6

computed() — derived state

Computed signals re-evaluate automatically when any of their dependencies change:

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

const price    = signal(100);
const quantity = signal(3);
const total    = computed(() => price() * quantity());

console.log(total()); // 300
price.set(120);
console.log(total()); // 360 — recalculated automatically

effect() — side effects

Run code whenever a signal's value changes:

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

const theme = signal('dark');

effect(() => {
    document.body.setAttribute('data-theme', theme());
});

theme.set('light'); // triggers the effect immediately

Effects must be created inside an injection context (a constructor, or via runInInjectionContext). They clean themselves up automatically when the component is destroyed.

A Practical Example: Shopping Cart

import { Component, signal, computed } from '@angular/core';

@Component({
    selector: 'app-cart',
    standalone: true,
    template: `
        <p>Items: {{ itemCount() }}</p>
        <p>Total: ${{ total() | number:'1.2-2' }}</p>
        <button (click)="addItem()">Add Widget</button>
    `
})
export class CartComponent {
    items = signal<{ name: string; price: number }[]>([]);

    itemCount = computed(() => this.items().length);
    total     = computed(() =>
        this.items().reduce((sum, item) => sum + item.price, 0)
    );

    addItem() {
        this.items.update(list => [...list, { name: 'Widget', price: 9.99 }]);
    }
}

No ChangeDetectorRef, no markForCheck(), no BehaviorSubject — the template just works.

Signals vs RxJS

These aren't competing technologies — they're complementary:

Conclusion

Signals are Angular's most significant DX improvement since standalone components. For new features, reach for signals first — they're simpler to reason about, easier to test, and faster at runtime. Keep RxJS where async streams genuinely need it, and use the interop utilities to bridge the two worlds cleanly.