effect(): reacting to changes

Key takeawayeffect() runs code every time a signal it reads changes. It is the tool for side effects (log, storage, DOM), never the one for computing a value — that is the role of computed().

After signal (the source) and computed (the derivation), here comes effect: the primitive that acts on the outside world when state changes. Logging, writing to localStorage, synchronizing a DOM element, firing analytics: all of this goes through effect().

How does an effect work?

effect() takes a function. Angular runs it a first time, notes the signals read, then re-runs it every time one of them changes:

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

const utilisateur = signal('Ada');

effect(() => {
  console.log('Current user:', utilisateur());
});
// Immediate log: "Current user: Ada"

utilisateur.set('Linus');
// Automatic log: "Current user: Linus"

As with computed, the dependencies are detected automatically: no need to list the watched signals.

Where do you declare an effect?

An effect() must be created in an injection context: most often in the constructor of a component/service, or in a field initializer. It is then automatically destroyed with the component — no manual unsubscribe.

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

@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <h1>Current theme: {{ theme() }}</h1>
    <button (click)="basculer()">Toggle theme</button>
  `,
})
export class AppComponent {
  readonly theme = signal<'clair' | 'sombre'>('clair');

  constructor() {
    effect(() => {
      const t = this.theme();
      document.body.style.background = t === 'sombre' ? '#111827' : '#ffffff';
      document.body.style.color = t === 'sombre' ? '#e5e7eb' : '#1f2937';
      localStorage.setItem('theme', t);
      console.log('Theme applied:', t);
    });
  }

  basculer(): void {
    this.theme.update((t) => (t === 'clair' ? 'sombre' : 'clair'));
  }
}

Try it live

Toggle the theme, open the console: the effect re-runs and applies the style to the body on every change.

effect-theme

Cleanup: onCleanup

An effect may need to clean up the previous run before re-running (cancel a timer, close a connection). The function receives an onCleanup for that:

effect((onCleanup) => {
  const id = setInterval(() => console.log('tick', compteur()), 1000);
  onCleanup(() => clearInterval(id)); // executed before the next run AND on destruction
});

The onCleanup callback is called before each re-run and on destruction of the effect: enough to avoid memory leaks.

When NOT to use effect()

This is the most important point of the chapter. effect() is often misused where a computed would do. Ask yourself: "am I computing a value, or am I acting on the outside?"

// ❌ Anti-pattern: using an effect to derive a value into another signal.
const prix = signal(100);
const ttc = signal(0);
effect(() => ttc.set(prix() * 1.2)); // fragile, redundant, hard to follow

// ✅ Correct: this is a derivation → computed.
const ttcOk = computed(() => prix() * 1.2);

Writing to a signal from within an effect is in fact forbidden by default (Angular throws an error) to protect you from infinite loops and circular dependencies.

Reserve effect() for real side effects:

  • logging, analytics, telemetry;
  • reading/writing in localStorage or sessionStorage;
  • direct DOM manipulation outside the template (focus, measurement, third-party library);
  • synchronization with a non-reactive imperative API.

untracked(): reading without creating a dependency

Sometimes you want to read a signal inside an effect without its modification re-running the effect. untracked() allows it:

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

effect(() => {
  const valeur = formulaire(); // tracked dependency
  // We read the user id without depending on it:
  const id = untracked(() => utilisateurId());
  console.log(`Saving form for ${id}`, valeur);
});

Here, the effect re-runs only when formulaire changes, not when utilisateurId changes.

Key points

  • effect() reacts to signal changes to produce side effects.
  • Create it in an injection context; destroyed automatically.
  • onCleanup cleans up before each re-run and on destruction.
  • Do not derive a value with an effect → use computed.
  • untracked() reads a signal without subscribing to it.

You have mastered the three primitives. Time for the fundamentals quiz to anchor them, before tackling communication between components.

We use Microsoft Clarity to understand how the site is used and improve it. By continuing to browse, you accept it. You can disable it at any time.