In Angular, which code runs in a reactive context, so a signal read there is tracked, and why is a click-handler read not?
answer
- an active consumer is set
- templates, computed, effects
- handlers run outside it
- only synchronous reads count
- untracked() opts out
basics
~20 sA 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 sTracking 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 linesimport { 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
Recall which places track reads: templates, computed, effects. Know that event handlers only read the current value.
Explain the active consumer, dynamic dependencies from the latest run, and why reads after await in an effect are lost.
Diagnose missing or surprising dependencies, use untracked() deliberately, and guard library code with assertNotInReactiveContext().
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.