skip to content

Effects & Their Timing

effect() re-runs a side effect when the signals it read change; root and view effects run at different points, and afterRenderEffect splits work into phases. Interviewers ask when an effect is wrong.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In Angular, what does effect() do, where must you call it, and when does its callback run for the first time?

level: juniorimportance: must knowfreq 70%

answer

  1. side effect, not derived state
  2. tracks what it read last time
  3. needs an injection context
  4. never synchronous on creation
  5. during change detection

basics

~20 s

effect() runs a side-effect callback that re-runs whenever a signal it read last time changes. It must be created in an injection context or given an injector, and it first runs asynchronously during the next change detection, not when created.

solid answer

~40 s

`effect(fn)` from `@angular/core` registers a callback that **always runs at least once** and then re-runs whenever a signal it read during its most recent run changes; dependencies are tracked dynamically, like `computed()`. It must be created in an **injection context**, typically a constructor or field initializer, or be given `{injector}`; otherwise Angular throws `NG0203`. Creating it inside a reactive context such as a `computed()` is rejected with `NG0602` in development mode. It never runs synchronously on creation: effects execute **asynchronously, as part of change detection**, so a component's effect first runs after its `ngOnInit` and before its template is checked. Effects are for syncing signal state to non-signal APIs, like writing a theme to `localStorage`, logging, or driving a chart library, not for deriving state. `effect()` has been stable since v20.

code

ts · 27 lines
ts
import {Component, Injector, effect, inject, signal} from '@angular/core';

@Component({
  selector: 'app-theme-toggle',
  template: `<button (click)="toggle()">{{ theme() }}</button>
    <button (click)="startLogging()">Log changes</button>`,
})
export class ThemeToggle {
  readonly theme = signal<'light' | 'dark'>('light');
  private readonly injector = inject(Injector);

  constructor() {
    // Injection context: fine. First run happens during change detection.
    effect(() => {
      localStorage.setItem('app-theme', this.theme());
    });
  }

  toggle() {
    this.theme.update(t => (t === 'light' ? 'dark' : 'light'));
  }

  startLogging() {
    // Not an injection context: without {injector} this would throw NG0203.
    effect(() => console.log('theme is', this.theme()), {injector: this.injector});
  }
}

go deeper

for a junior

Recall that effect() re-runs a side effect when signals it read change, that it belongs in a constructor or field initializer, and that it suits syncing to localStorage or logging.

for a middle

Explain that effects run asynchronously during change detection, after ngOnInit for a component, and why several writes collapse into one run.

for a senior

Show how you create effects outside constructors with an injector, guard browser-only APIs under server-side rendering, and test effects by driving change detection.

for a principal

Frame effects as an escape hatch: decide which side effects the team routes through effect() and which belong in event handlers or derived state instead.

## What an effect is in Angular An **effect** is an operation that runs whenever one or more signal values it depends on change. You create one with `effect()` from `@angular/core`: ```ts effect(() => { localStorage.setItem('theme', this.theme()); }); ``` The rules that define it: - It **always runs at least once**, so it can establish its dependencies. - On each run it **tracks the signals it reads**. Only reads from the most recent run count, so dependencies are dynamic, as with `computed()`. - When any tracked signal changes, the effect is **scheduled**, not run on the spot. - It returns an **`EffectRef`** whose `destroy()` stops it. `effect()` spent several releases in developer preview and became stable API in **v20**; its timing was reworked in v19. ## Where you may create one `effect()` needs an **injection context**, because it looks up the injector to decide what kind of effect it is and when to destroy it. | Where you call it | Works? | |---|---| | component, directive or service constructor | yes | | class field initializer | yes | | a method called later, such as a click handler | only with `{injector}` passed in options | | inside `computed()` or another effect's callback | no: development builds throw `NG0602`, not allowed in a reactive context | Outside an injection context and without an `injector` option, Angular throws **`NG0203`** ("can only be used within an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`"). Passing `{injector: this.injector}`, where the injector came from `inject(Injector)`, is the documented way to create an effect later. ## When it runs Effects **always execute asynchronously, during change detection**. Nothing runs inside the `effect()` call itself. 1. The effect is created dirty, so it is due to run at the next synchronization. 2. For an effect created by a **component** (or a directive, or a service provided in a component's injector), the first run happens while Angular refreshes the view that hosts the component: after the component's `ngOnChanges`, `ngOnInit` and `ngDoCheck`, and **before its own template is checked**. Inputs are therefore already set. 3. For an effect created by a **root service**, the run happens at the start of the next application tick, before any view is checked. 4. Later, a write to any tracked signal marks the effect dirty, which asks the scheduler for change detection; the effect runs at that pass. With zoneless the default since v21, that notification is what triggers the pass. A practical consequence: in a unit test, creating a component and asserting on `localStorage` immediately fails; the effect has not run until change detection has. ## What effects are for The Angular guide calls effects "the last API you reach for". Appropriate uses sync signal state **out** to something that is not a signal: - persisting to `localStorage`, `sessionStorage` or cookies; - logging and analytics; - imperative DOM work that template syntax cannot express; - driving a canvas, chart or other third-party UI library. What they are **not** for is propagating state between signals. Copying one signal into another from an effect invites `ExpressionChangedAfterItHasBeenChecked` errors, circular updates and extra change detection; derived values belong in `computed()`, and derived-but-overridable ones in `linkedSignal()`. ## A theme example, end to end ```ts @Injectable({providedIn: 'root'}) export class ThemeStore { readonly theme = signal<'light' | 'dark'>('light'); constructor() { effect(() => { localStorage.setItem('app-theme', this.theme()); }); } } ``` This is a **root effect**: it runs on the first tick after the service is created and again after each `theme.set(...)`. Two cautions: effects also run during server-side rendering, where `localStorage` does not exist, so guard browser-only APIs; and several `set()` calls between two passes produce **one** run with the latest value, not one run per write. ## Common misconceptions - "`effect()` runs synchronously on every write." It is scheduled and runs once per pass with the current values. - "It needs a dependency array." Dependencies are whatever it read last time. - "It can be created anywhere." It needs an injection context or an `injector`. - "Effects are the way to keep two signals in sync." That is a `computed()` or `linkedSignal()`.

  • In Angular, if a click handler calls theme.set() three times, how many times does an effect reading theme run?
    Usually once. Each write only marks the effect dirty; it runs at the next change detection pass and reads the current value then, so the intermediate values are never seen. That is why an effect is a poor fit for anything that needs every individual change.
  • Why does Angular throw NG0602 in development mode when effect() is called inside a computed()?
    `effect()` asserts, in development builds, that it is not running inside a reactive context. Creating effects from a computation or another effect would create a new effect every time that function re-ran, leaking effects. Create effects once, in a constructor or with an explicit injector.
  • Does an Angular effect run during server-side rendering?
    Yes. Plain `effect()` has no browser-only guard and runs during server change detection, so it must not touch `localStorage`, `window` or the DOM unguarded. `afterRenderEffect()`, by contrast, runs only in the browser.

saying these in an interview costs you the question

  • effect() runs its callback synchronously the moment it is created.
  • An effect runs once for every set() on a signal it reads.
  • You must list an effect's dependencies, as in a dependency array.
  • effect() can be called from any method without passing an injector.
  • Effects are the standard way to keep one signal in sync with another.
open as a page

In Angular, what do an effect's onCleanup callback, the manualCleanup option and EffectRef.destroy() each control?

level: middleimportance: should knowfreq 38%

basics

~20 s

onCleanup registers a function that runs before the effect's next run and when it is destroyed. manualCleanup: true stops Angular from registering the effect with the injector's DestroyRef. EffectRef.destroy() stops the effect and runs its pending cleanup.

open as a page

In Angular, how does the timing of an effect created in a component differ from one created in a root service?

level: middleimportance: should knowfreq 40%

basics

~20 s

A view effect, created by a component, directive or component-provided service, runs just before that component's template is checked. A root effect, created by a root service, is flushed at the start of each application tick, before any view is checked.

open as a page

An Angular app sets an effectiveTheme signal from an effect() reading prefersDark() and theme(); it causes extra change detection passes and once looped. Why, and what replaces it?

level: seniorimportance: should knowfreq 48%

basics

~20 s

The effect copies derived state, so the value exists only after the effect runs and writes it; readers already checked must be checked again, and an effect reading what it writes can re-trigger itself. Replace it with computed(), or linkedSignal() if overridable.

open as a page

As a lead, what rule would you set for when an Angular team may use effect(), and how would you enforce it?

level: principalimportance: should knowfreq 24%

basics

~20 s

Allow effect() only to push signal state to non-signal APIs such as storage, logging, DOM or third-party libraries. Derived values use computed(), overridable ones linkedSignal(), async loads a resource, user intent an event handler; any signal write in an effect needs justification.

open as a page

In Angular, when should you use afterRenderEffect() instead of effect(), and how do its earlyRead, write, mixedReadWrite and read phases run?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

Use afterRenderEffect() when signal-driven work must touch the rendered DOM: it runs after Angular commits changes, only in the browser. Phases run earlyRead, write, mixedReadWrite, read; each receives the previous phase's result as a signal.

open as a page