Signals: signal and computed

Angular · Essentials · 12 min

Signals are the foundation of modern Angular reactivity and change detection.

A signal is a wrapper around a value that notifies interested consumers when it changes.

count = signal(0);        // WritableSignal<number>
count();                  // read: call it like a function
count.set(5);             // replace the value
count.update((c) => c + 1);   // compute from the previous value

`computed` creates a read-only signal derived from others:

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

Computed signals are lazy and memoised: the function runs only when the value is read after one of its dependencies changed. Dependencies are tracked automatically from the signals read during the last run, so conditional branches create dynamic dependencies.

When a template reads a signal, Angular knows exactly which component to refresh when it changes. That is what makes zoneless, OnPush-by-default Angular work.

Rules that prevent bugs:

  • Don't mutate values in place. this.items().push(x) changes the array without notifying anyone. Use update with a new array or object: items.update((list) => [...list, x]).
  • Signals compare with Object.is by default; setting an equal value does nothing. Pass a custom equal option if needed.
  • Keep computed functions pure: no setting other signals and no side effects inside them.
  • Expose read-only views with asReadonly() so only the owner can write.

Example

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

type Item = { name: string; price: number };

@Component({
  selector: 'app-cart',
  template: `
    <p>{{ items().length }} items, total {{ total() }}</p>
    <button (click)="add()">Add</button>
    <button (click)="discount.set(0.1)">10% off</button>
  `,
})
export class Cart {
  protected items = signal<Item[]>([{ name: 'Pen', price: 2 }]);
  protected discount = signal(0);
  protected total = computed(() => {
    const sum = this.items().reduce((s, i) => s + i.price, 0);
    return Math.round(sum * (1 - this.discount()) * 100) / 100;
  });

  protected add() {
    // this.items().push(...) would NOT update the view
    this.items.update((list) => [...list, { name: 'Pad', price: 5 }]);
  }
}

Remember

  • Read a signal by calling it; write with set or update
  • computed is lazy, memoised and tracks dependencies automatically
  • Update arrays and objects immutably; in-place mutation notifies nobody

Watch out for

  • Mutating an array or object inside a signal and expecting the UI to update.

Check yourself

  1. `items = signal([1, 2])`, then `items().push(3)`. A template shows items().length. What does it show?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/guide/signals