Effects
Signals and computed values let you derive state, but eventually you need to do something with that state — log it, sync it to localStorage, update the DOM imperatively, or call an external API. That is what effect() is for. An effect is a piece of code that runs once immediately and then re-runs automatically whenever any signal it reads changes. It is Angular’s bridge between the reactive world of signals and the imperative world of side effects.
Creating an effect
You create an effect by calling the effect() function from @angular/core and passing it a callback. Angular runs the callback right away, tracks every signal read during that run, and schedules the callback to run again whenever any of those signals change.
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `<button (click)="count.set(count() + 1)">{{ count() }}</button>`,
})
export class CounterComponent {
count = signal(0);
constructor() {
effect(() => {
console.log(`The count is now ${this.count()}`);
});
}
}
Output:
The count is now 0
The count is now 1
The count is now 2
The first line appears immediately when the component is created; the rest appear as the button is clicked. Notice you never tell the effect which signals to watch — dependency tracking is automatic and dynamic. Only signals actually read on a given run are tracked, so a branch that is not executed contributes no dependencies.
Injection context requirement
effect() must be called within an injection context — typically a component, directive, or service constructor, or a field initializer. This lets Angular tie the effect’s lifecycle to the surrounding component so it is automatically destroyed when the component is destroyed, preventing leaks.
@Component({ /* ... */ })
export class ProfileComponent {
// Field initializer — also a valid injection context.
private logger = effect(() => console.log(this.user()));
user = signal('Ada');
}
If you must create an effect outside of a constructor or initializer — for example inside a lifecycle hook or a method — pass an Injector explicitly:
import { Injector, inject } from '@angular/core';
export class LateComponent {
private injector = inject(Injector);
ngOnInit() {
effect(() => console.log('runs later'), { injector: this.injector });
}
}
Calling
effect()without an available injection context throwsNG0203. When in doubt, create effects in the constructor.
Cleaning up with onCleanup
Effects often start something that needs tearing down before the next run — a timer, a subscription, an event listener. The effect callback receives an onCleanup registration function. Pass it a teardown function and Angular runs it before the next execution of the effect and once more when the effect is destroyed.
@Component({ /* ... */ })
export class TimerComponent {
delay = signal(1000);
constructor() {
effect((onCleanup) => {
const ms = this.delay();
const id = setInterval(() => console.log('tick'), ms);
onCleanup(() => clearInterval(id));
});
}
}
Each time delay changes, the previous interval is cleared before a new one is created — no orphaned timers.
Manually destroying an effect
effect() returns an EffectRef. Call its destroy() method to stop the effect early, before its host component is destroyed.
const ref = effect(() => console.log(this.value()));
// later…
ref.destroy();
Writing to signals from effects
By default, an effect may not write to a signal it also reads — this guards against accidental infinite loops. If you have a deliberate reason to set a signal from inside an effect, opt in with allowSignalWrites.
effect(() => {
this.history.update((h) => [...h, this.current()]);
}, { allowSignalWrites: true });
Prefer
computed()for deriving state and reserve writes-from-effects for genuine side effects. Most “I need to write a signal in an effect” cases are really computed values in disguise.
effect() options reference
| Option | Type | Purpose |
|---|---|---|
injector | Injector | Provide an injection context when not calling from a constructor or field initializer. |
manualCleanup | boolean | Skip automatic teardown so the effect lives until destroy() is called manually. |
allowSignalWrites | boolean | Permit writing to signals inside the effect callback. |
Best practices
- Derive state with
computed(); useeffect()only for true side effects like logging, DOM work, storage, or analytics. - Create effects in the constructor or a field initializer so they live in a valid injection context and are auto-destroyed.
- Always register an
onCleanupfor anything you start (timers, listeners, subscriptions) to avoid leaks. - Avoid
allowSignalWritesunless the write is intentional and cannot be expressed as a computed value. - Keep effect callbacks fast and synchronous; offload heavy or async work and let the effect just trigger it.
- Pass an explicit
injectorwhen an effect must be created outside the constructor, rather than fightingNG0203.