Angular – computed and effect

July 6, 20264 min readUpdated 8/21/2026

Most state in an application is not stored; it is worked out from state that is. Signals have a dedicated tool for that, and using it removes a whole category of bug.

computed()

readonly totals = computed(() => calculateTotals(this._items(), this._orderType()));
readonly itemCount = computed(() => this.totals().itemCount);

The cart's totals are derived, never stored. There is one source of truth — the items and the order type — and the subtotal, tax, delivery fee and total are all functions of it. A stored total is a second source of truth that can disagree with the first, and eventually will.

Two properties matter:

Dependencies are tracked automatically. Reading this._items() inside the function is what subscribes it. There is no dependency array, so there is no way to forget one — which is the failure mode useMemo is famous for.

It is lazy and cached. The function does not run when the dependencies change; it runs when someone next reads the computed, and only if a dependency actually changed since. Nothing reading totals() means calculateTotals never runs. And itemCount computed from totals — a computed reading a computed — is entirely normal.

⚠️ A computed must be pure. No writing to signals, no HTTP calls, no console.log you rely on. It may be skipped, or run at a moment you did not predict, because the framework decides when. Anything with a side effect belongs in an effect.

effect()

An effect runs a side effect when the signals it read last time change:

const ref = effect(() => {
  if (this.menu.loading()) return;

  // Read the catalogue now, while inside the reactive context, then leave it.
  const products = this.menu.products();
  const crusts = this.menu.crusts();

Again there is no dependency array — reading menu.loading() is what subscribes it. That is the whole API.

Effects run after change detection, and they are batched: several signals changing in one tick produce one run.

onCleanup

The effect callback receives a cleanup registrar, which runs before the next run and on destroy. The cart uses it to debounce writes to the server:

const timer = setTimeout(() => void this.save(items, orderType), 300);
onCleanup(() => clearTimeout(timer));

Clicking "+" three times quickly is one write, not three: each change cancels the pending timer before scheduling a new one. It is the return-a-function cleanup of a React useEffect, under a different name.

Cleanup of the effect itself

this.destroyRef.onDestroy(() => ref.destroy());

An effect created in a component or service's injection context — a field initialiser or the constructor — is destroyed with it automatically. This one is created inside a method called from the constructor, so it is tied to the service's lifetime explicitly. If you ever call effect() outside an injection context you must pass an injector or keep the handle; otherwise it outlives its owner.

The rule about writing to signals

Writing to a signal inside an effect is where infinite loops come from: the write changes a signal, which re-triggers the effect, which writes again. Angular used to throw on this by default. It is now allowed and still worth avoiding — if an effect writes a signal, ask whether the value is actually derived, in which case it wants to be a computed.

The cart shows the disciplined version of the unavoidable case. Its effects need flags, and those flags are plain fields, not signals:

/** Guards the hydrate effect. A plain field, so setting it cannot re-trigger the effect. */
private hydrationStarted = false;

The comment says exactly why. Reading a signal inside an effect subscribes to it, so guarding on a signal and then setting it would schedule the effect to run a second time only to discover it has nothing left to do. The same reasoning applies to the cart id:

private cartId: string | null = cartIdStore.get();

The persist effect needs the current cart id but must not re-run when it is assigned — that would fire an extra PUT immediately after the cart is created. A signal read inside an effect becomes a dependency; a plain field does not. It is the direct equivalent of a useRef, for exactly the same reason.

Which one to reach for

Deriving a value? computed. Always, and it is almost always what you want.

Reaching outside Angular? effect. Writing to localStorage, syncing to a server, driving a third-party library, logging.

If you are reaching for an effect to keep one piece of state in step with another, that is the signal to stop: the second piece is derived, and a computed will do it without the synchronisation bug you are about to write.

linkedSignal

The case that genuinely falls between them: a value derived from another, but locally overridable — a selected size that follows the product until the user picks one. computed cannot do it because it is read-only, and an effect writing a signal is the bug above. linkedSignal is built for it. The pizza app never needed one, which is worth knowing: most apps do not.

What is next

The lifecycle hooks — and, more usefully, how few of them you still need.