Skip to content
Angular ng signals 4 min read

Computed Signals

A computed signal is a read-only signal whose value is derived from one or more other signals. Instead of storing state, it describes how to calculate a value, and Angular keeps that result in sync automatically. Computed signals are lazy and memoized: they only recalculate when something they read actually changes, and only when their value is next read. This makes them the idiomatic way to model derived state without duplicating data or wiring up manual subscriptions.

Creating a computed signal

Call computed() with a derivation function. Inside that function you read any number of signals; whichever ones you read become the computed’s dependencies. The result is a Signal<T> you call like any other signal.

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

const price = signal(100);
const quantity = signal(3);

const subtotal = computed(() => price() * quantity());

console.log(subtotal()); // reads dependencies and caches the result
quantity.set(5);
console.log(subtotal()); // recomputes because quantity changed

Output:

300
500

You never call .set() or .update() on a computed — its value is owned by the derivation function. The return type is a plain Signal<number>, not a WritableSignal, so the compiler prevents you from mutating it directly.

Laziness and memoization

Computed signals do not run eagerly. The derivation function executes the first time you read the signal, then caches the result. Subsequent reads return the cached value with no work at all. The function only re-executes when one of its dependencies changes and the computed is read again.

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

const width = signal(4);
const height = signal(5);

const area = computed(() => {
  console.log('computing area');
  return width() * height();
});

console.log(area()); // logs "computing area", returns 20
console.log(area()); // cached, no log, returns 20
width.set(8);
console.log(area()); // logs "computing area", returns 40

Output:

computing area
20
20
computing area
40

This means a computed signal that is never read never runs, and one whose dependencies are stable never recomputes — a free performance win compared to recalculating in a getter on every change detection cycle.

Dynamic dependencies

Dependencies are tracked per execution, not declared up front. If a branch of your derivation does not run, the signals it would have read are not tracked. This keeps the dependency graph tight and accurate.

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

const showTax = signal(false);
const subtotal = signal(200);
const taxRate = signal(0.2);

const total = computed(() =>
  showTax() ? subtotal() * (1 + taxRate()) : subtotal()
);

While showTax() is false, total does not depend on taxRate — changing the tax rate will not invalidate the cached value. The moment showTax flips to true, the next read re-tracks taxRate and picks up its changes.

Computed signals in components

Computed signals shine in standalone components for presentation logic. Read them directly in the template with the new control flow; Angular updates only the bindings that depend on a changed value.

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

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    <p>Items: {{ count() }}</p>
    <p>Total: {{ total() | currency }}</p>
    @if (isEmpty()) {
      <p>Your cart is empty.</p>
    }
    <button (click)="add()">Add item</button>
  `,
})
export class CartComponent {
  protected readonly items = signal<number[]>([]);

  protected readonly count = computed(() => this.items().length);
  protected readonly total = computed(() =>
    this.items().reduce((sum, price) => sum + price, 0)
  );
  protected readonly isEmpty = computed(() => this.count() === 0);

  add(): void {
    this.items.update(list => [...list, 25]);
  }
}

Note that count, total, and isEmpty form a small graph: isEmpty depends on count, which depends on items. Updating items once correctly invalidates all three.

computed() vs. a getter or method

A common alternative is a class getter or template method call. Both recompute on every change detection pass, regardless of whether their inputs changed. A computed signal recomputes only when needed.

AspectMethod / gettercomputed()
RecomputationEvery CD cycleOnly when a dependency changes
CachingNoneMemoized result
Dependency trackingManual / noneAutomatic on read
Zoneless friendlyNoYes
Write accessN/ARead-only by design

Custom equality

Like writable signals, a computed uses Object.is by default to decide whether its new result differs from the cached one. If the derivation produces value objects, supply an equal function so downstream consumers are not notified for equivalent results.

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

const user = signal({ first: 'Ada', last: 'Lovelace' });

const fullName = computed(
  () => `${user().first} ${user().last}`,
  { equal: (a, b) => a === b }
);

Warning: Keep derivation functions pure. Do not call .set()/.update() on other signals, perform HTTP requests, or trigger side effects inside computed() — Angular throws if you write to a signal during computation. Use effects for side effects instead.

Best Practices

  • Use computed() for any value that can be derived from existing signals rather than storing it as separate writable state.
  • Keep derivation functions pure and synchronous — no side effects, no signal writes, no async work.
  • Let computed signals compose: build small, focused derivations that read other computeds instead of one large function.
  • Prefer a computed over a template method or getter so work runs only when dependencies actually change.
  • Supply a custom equal function when the result is an object whose identity changes but whose meaning does not.
  • Read computed signals directly in templates; they integrate with zoneless change detection for fine-grained updates.
Last updated June 14, 2026
Was this helpful?