skip to content

Signals

Angular's signal primitives, signal(), computed(), effect(), linkedSignal() and resource(), and the producer/consumer graph behind them. Interviewers probe derivation, effect timing and async state.

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

explore

questions

26

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 does linkedSignal() give you that neither a plain signal() nor a computed() does?

level: juniorimportance: must knowfreq 50%

basics

~20 s

linkedSignal() is a writable signal whose value is initialised and reset by a computation. You can set() it like a signal(), but whenever the signals its computation reads change, it recomputes and replaces whatever was set.

open as a page

In Angular, how does resource() turn a selected user id signal into loaded order history, and what do its params and loader options do?

level: juniorimportance: must knowfreq 52%

basics

~20 s

resource() pairs a reactive params function with an async loader: whenever the signals read in params produce a new value, Angular calls the loader with it and exposes the result through signals such as value(), status() and isLoading().

open as a page

In an Angular component, why derive a cart total with computed() instead of a getter or a method called from the template?

level: juniorimportance: must knowfreq 72%

basics

~20 s

computed() is lazy and memoized: its function runs on first read and its result is cached until a signal it read changes. A template getter or method reruns on every check of that view, and nothing caches its result.

open as a page

In Angular, how do you create a writable signal with signal(), read its value, and change it with set() versus update()?

level: juniorimportance: must knowfreq 78%

basics

~20 s

signal(initialValue) returns a WritableSignal: a getter function you call, such as qty(), to read the value. set(value) replaces the value outright, while update(fn) computes the next value from the current one and must return it.

open as a page

In Angular, what happens in the signal graph when you call set() on a signal: which work is pushed immediately and which is pulled later?

level: middleimportance: must knowfreq 52%

basics

~20 s

A set() that changes the value only pushes a dirty mark through the graph's live consumers; no computed or effect runs then. Recomputation is pulled later, when something reads a computed, and only if a producer's version changed.

open as a page

In Angular, what does each resource status value mean, and what does value() return in each state, including after the loader fails?

level: middleimportance: must knowfreq 50%

basics

~20 s

A resource reports idle, loading, reloading, resolved, error or local. value() is undefined (or defaultValue) when idle or loading, keeps the old data while reloading, and throws in the error state, so guard reads with hasValue().

open as a page

In Angular, which code runs in a reactive context, so a signal read there is tracked, and why is a click-handler read not?

level: juniorimportance: should knowfreq 46%

basics

~20 s

A reactive context is code running while Angular has set an active consumer: template rendering, computed() and linkedSignal() computations, effect() and afterRenderEffect() callbacks, and a resource's params. A click handler runs with no active consumer, so its reads just return values.

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

In Angular, greeting reads firstName and a fullName computed built from firstName and lastName; why does greeting never see a half-updated name?

level: middleimportance: should knowfreq 40%

basics

~20 s

Angular never recomputes during a write: firstName.set() only marks fullName and greeting dirty. When greeting is read, it polls its producers and brings fullName up to date first, so it always runs once with the new firstName and the new fullName together.

open as a page

In Angular, when a computed recomputes to an equal value, what happens to the computeds, effects and templates that depend on it?

level: middleimportance: should knowfreq 36%

basics

~20 s

Its version is not bumped, so dependants that poll it see no change: downstream computeds keep their cached values, a dirty effect skips its body, and a dirty template consumer does not refresh its view.

open as a page

In Angular, how does linkedSignal()'s {source, computation} form use the previous value to keep a still-valid selection when its source changes?

level: middleimportance: should knowfreq 40%

basics

~10 s

With linkedSignal({source, computation}), the computation receives the new source value and a previous object holding the old source and the linked signal's current value. It can return previous.value when still valid instead of resetting.

open as a page

In Angular, how does rxResource() differ from resource() when the order-history service you call already returns an Observable?

level: middleimportance: should knowfreq 42%

basics

~20 s

rxResource() from @angular/core/rxjs-interop takes a stream function returning an Observable instead of a promise loader; it subscribes per params value, updates value() on every emission and unsubscribes when params change, while exposing the same Resource API.

open as a page

In Angular, what does the equal option on signal() and computed() control, and when is a custom equality function worth it?

level: middleimportance: should knowfreq 34%

basics

~20 s

The equal option replaces the default Object.is check that decides whether a new value counts as a change. When it returns true, the old value is kept and dependents are not notified, which suits derivations that rebuild equivalent objects.

open as a page

In Angular, why does pushing a line item into an array held by a signal leave a computed() total and the view unchanged?

level: middleimportance: should knowfreq 62%

basics

~20 s

Pushing edits the array without telling the signal, and even set() or update() with that same array is equal under the default Object.is check. No dependent is notified, so the computed keeps its cached total. Return a new array instead.

open as a page

In Angular, what does untracked() change about signal reads inside a reactive function such as computed(), and when should you use it?

level: middleimportance: should knowfreq 40%

basics

~20 s

untracked(fn) runs fn with dependency tracking switched off and returns its result, so signals read inside it do not become dependencies. Use it for values you deliberately want to sample, never to hide a real input.

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

In Angular, how does reading a signal in a component's template make that view refresh after a write, and which views does the write mark?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Each component view gets a live template consumer that records the signals its template reads. A write marks that consumer dirty, notifies the change-detection scheduler and flags ancestors only for traversal, so the next pass refreshes that component without re-rendering its OnPush parents.

open as a page

An Angular checkout resets selectedShipping to the first option inside an effect() when the country changes; what goes wrong, and why does linkedSignal() fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The effect resets the selection only when it runs, so reads in between pair the new country with a stale option, and reading the selection inside the effect makes each user choice re-trigger the reset. linkedSignal() recomputes on read, with no stale window.

open as a page

When an Angular resource() loading order history switches users mid-request, what happens to the old response, and why still pass abortSignal to the loader?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Angular aborts the superseded load and discards its result, so a slow response for the previous user never overwrites the new one. Passing abortSignal to fetch lets that abort cancel the network request itself.

open as a page

You are migrating an Angular cart service from a BehaviorSubject with map() pipes to signal() and computed(); what maps to what, and what behaviour changes?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The subject becomes a private signal(), next() becomes set() or update(), asObservable() becomes asReadonly(), and derived map() streams become computed(). Behaviour changes: equal writes stop notifying, derived values are pulled lazily, and intermediate values are not delivered.

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

In Angular, why might a linkedSignal() reset the user's shipping choice even though the list of options looks unchanged?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

A linkedSignal recomputes when any signal its function reads reports a change. A source that rebuilds the same list as a new array counts as changed, and so does any extra signal read in the shorthand or computation.

open as a page

In Angular 22, how does debounced() delay a filter signal before it feeds a resource(), and what does it report while waiting?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

debounced(source, wait) returns a read-only Resource whose value() is the last settled value of the source signal; while the wait runs it reports 'loading' and keeps the old value, so a resource reading it in params only reloads once input settles.

open as a page