effect(): reacting to changes
Key takeaway —
effect()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 ofcomputed().
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
localStorageorsessionStorage; - 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.
onCleanupcleans 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.