skip to content

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%

answer

  1. an active consumer is set
  2. templates, computed, effects
  3. handlers run outside it
  4. only synchronous reads count
  5. untracked() opts out

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.

solid answer

~40 s

Tracking works through an **active consumer**: while Angular runs a template refresh, a `computed()` or `linkedSignal()` computation, an `effect()` or `afterRenderEffect()` callback, or a resource's `params` function, it sets that node as the active consumer, and every signal read records a dependency on it. A `(click)` handler, a `setTimeout` callback, an Observable `subscribe` callback or a constructor runs with no active consumer, so reading `greeting()` there returns the current value and records nothing — the handler will not rerun when `greeting` changes, and it should not. Tracking is also synchronous only: reads after an `await` inside an effect are not tracked. Only signals actually read on the latest run are dependencies, and `untracked()` deliberately reads without tracking.

code

ts · 28 lines
ts
import { Component, computed, effect, signal, untracked } from '@angular/core';

@Component({
  selector: 'app-greeting-log',
  template: `
    <p>{{ greeting() }}</p>
    <button (click)="logGreeting()">Log</button>
  `,
})
export class GreetingLog {
  readonly firstName = signal('Ada');
  readonly visits = signal(0);
  readonly greeting = computed(() => `Hi ${this.firstName()}`);

  constructor() {
    effect(() => {
      // Tracked: reruns when greeting changes.
      // Not tracked: visits is read through untracked().
      console.log(this.greeting(), untracked(this.visits));
    });
  }

  logGreeting(): void {
    // Event handler: no active consumer, so this read tracks nothing.
    console.log('clicked:', this.greeting());
    this.visits.update((v) => v + 1);
  }
}

go deeper

for a junior

Recall which places track reads: templates, computed, effects. Know that event handlers only read the current value.

for a middle

Explain the active consumer, dynamic dependencies from the latest run, and why reads after await in an effect are lost.

for a senior

Diagnose missing or surprising dependencies, use untracked() deliberately, and guard library code with assertNotInReactiveContext().

for a principal

Define team rules for where signal reads belong, so state flows through templates and computeds rather than copies in handlers and hooks.

## What "reactive context" means Angular's signal functions do not subscribe to anything when you call them. A read such as `greeting()` checks one thing: is there an **active consumer** right now? If there is, the read records an edge from `greeting` to that consumer. If there is not, the read simply returns the value. Code that runs while an active consumer is set is said to run in a **reactive context**. This answer assumes Angular 22. ## Where Angular sets an active consumer The signals guide lists the places Angular enters a reactive context: - rendering a component **template**, including host bindings; - evaluating a **`computed()`**; - evaluating a **`linkedSignal()`**; - running an **`effect()`** or **`afterRenderEffect()`** callback; - evaluating a **resource's `params`** function. A resource's loader runs inside `untracked`, so its reads are not dependencies. ## Where it does not - **Event handlers** such as `(click)="save()"`: Angular calls them in response to DOM events, outside any refresh. - **Timers and callbacks**: `setTimeout`, `requestAnimationFrame`, promise `.then()`, Observable `subscribe` callbacks. - **Constructors and lifecycle code** that copy values out of signals. - **Code after an `await`** inside an effect. The context is only active for the synchronous part; reads after the first `await` are not tracked. A read in these places is correct and common — a click handler reading `this.firstName()` to build a request is fine. It simply creates no subscription, which is what you want: a handler should run when the user clicks, not when state changes. ## A worked example ```ts readonly greeting = computed(() => `Hi ${this.firstName()}`); logGreeting(): void { // (click) handler console.log(this.greeting()); // current value, nothing tracked } constructor() { effect(() => console.log(this.greeting())); // tracked: reruns on change } ``` Clicking logs the greeting once per click. Changing `firstName` reruns the effect, never the handler. ## Dynamic dependencies A consumer's dependencies are the signals it read on its **most recent** run, not every signal mentioned in its code: ```ts readonly badge = computed(() => this.showFullName() ? this.fullName() : this.firstName(), ); ``` With `showFullName()` false, `badge` depends on `showFullName` and `firstName` only; changing `lastName` does not touch it. Angular rebuilds the dependency list on each run and drops links that were not read again. ## Tools around the context | Tool | What it does | | --- | --- | | `untracked(fn)` or `untracked(signal)` | Reads without recording a dependency, even inside a reactive context | | `assertNotInReactiveContext(fn)` | Throws if called while a consumer is active; for library code that must not run there | | Error `NG0600` | Thrown when code writes a signal while a `computed` or a template is being evaluated | ## Common mistakes 1. Expecting a value read in `ngOnInit` and stored in a field to update the template later. Read the signal in the template instead. 2. Expecting a subscription callback that reads a signal to rerun when that signal changes. 3. Reading a signal after `await` in an effect and wondering why the effect ignores it. 4. Treating a missing dependency as a bug in a click handler, when handlers are deliberately untracked.

  • Why does a signal read after await inside an effect not become a dependency?
    The active consumer is set only while the effect's function runs synchronously. After the first `await`, the rest runs later in a microtask, when the effect is no longer the active consumer, so those reads record nothing. Read the signals before the `await` and keep the values in local variables.
  • What happens if a computed writes to a signal?
    Angular throws `NG0600`: writes are not allowed while a `computed` or a template is being evaluated, because those are meant to be pure reads. Derive the value with another `computed()` instead.

saying these in an interview costs you the question

  • Reading a signal anywhere subscribes the surrounding code to it.
  • A click handler that reads a signal reruns when the signal changes.
  • Every signal mentioned in a computed is a dependency forever.
  • Reads after await inside an effect are still tracked.
  • A value copied from a signal in ngOnInit updates the template later.