Skip to content
Angular ng signals 4 min read

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 throws NG0203. 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

OptionTypePurpose
injectorInjectorProvide an injection context when not calling from a constructor or field initializer.
manualCleanupbooleanSkip automatic teardown so the effect lives until destroy() is called manually.
allowSignalWritesbooleanPermit writing to signals inside the effect callback.

Best practices

  • Derive state with computed(); use effect() 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 onCleanup for anything you start (timers, listeners, subscriptions) to avoid leaks.
  • Avoid allowSignalWrites unless 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 injector when an effect must be created outside the constructor, rather than fighting NG0203.
Last updated June 14, 2026
Was this helpful?