skip to content

In Angular, why is an @ViewChild result undefined in ngOnInit, and when do decorator and signal query results actually become available?

level: middleimportance: must knowfreq 68%

answer

  1. never in the constructor
  2. dynamic resolves before AfterViewInit
  3. static: true trades updates for earliness
  4. signals: read reactively, not once

basics

~20 s

Decorator queries are dynamic by default and resolve just before ngAfterContentInit or ngAfterViewInit; static: true resolves them before ngOnInit but never updates them. Signal queries have no static option and update themselves, so read them reactively.

solid answer

~40 s

A decorator query such as `@ViewChild(Chart)` defaults to `static: false`: Angular resolves it after the view's nodes, including `@if` and `@for` content, have been processed, so the property is set just before `ngAfterViewInit` (content queries before `ngAfterContentInit`). In `ngOnInit` it is still `undefined`. `{static: true}` promises the node is never conditional, so Angular resolves it when the view is created, before `ngOnInit`, but the result then never updates. Signal queries (`viewChild()` and friends) have no `static` option: they return `undefined` or an empty array until results are collected, then update whenever matches change. Nothing is available in the constructor. Read signal queries in the template, a `computed()`, an `effect()` or later hooks rather than once during initialisation.

go deeper

for a junior

Remember that a default @ViewChild is undefined in ngOnInit and set by ngAfterViewInit, and that nothing exists in the constructor.

for a middle

Explain dynamic versus static resolution, why static breaks conditional nodes, and how signal queries avoid the choice by updating themselves.

for a senior

Remove timing bugs by deriving from query signals with computed rather than caching results in hooks, and diagnose NG0951 and NG0100 when they appear.

for a principal

Plan a codebase move from decorator to signal queries, using the signal-queries migration, and decide where the remaining static flags are really justified.

## The classic symptom A component declares `@ViewChild(Chart) chart!: Chart;` and calls `this.chart.resize()` in `ngOnInit`. It throws, because `chart` is `undefined`. The same call in `ngAfterViewInit` works. The reason is when Angular fills in query results, and the answer differs between decorator and signal queries. ## Decorator queries: dynamic by default `@ViewChild` and `@ContentChild` take a `static` option. The plural decorators do not. | Option | Resolved | Updates later? | Can match inside `@if`/`@for`? | |---|---|---|---| | `static: false` (default) | view queries before `ngAfterViewInit`, content queries before `ngAfterContentInit` | yes, on each check | yes | | `static: true` | once the view is created, before `ngOnInit` | **never** | no, so it would stay `undefined` | A **dynamic** query waits until change detection has processed the template, including embedded views created by control flow, because only then does Angular know whether a conditional node exists. A **static** query is your promise that the node is always there, which lets Angular resolve it from the creation pass alone. If you mark a query `static: true` and the node is inside an `@if`, the result is `undefined` and stays so. `@ViewChildren` and `@ContentChildren` are always dynamic; their `QueryList` is populated before the matching `After...Init` hook. ## Signal queries: no static, always live `viewChild()`, `viewChildren()`, `contentChild()` and `contentChildren()` return signals. In Angular 22.2 the implementation returns an empty result while the query does not exist yet or while the view is still in its first creation pass, so partial results are never exposed. After that, each read reflects the current matches, and the signal notifies dependants when matches change. Practical rules: - **Never in the constructor.** The query has not been set up; a required query read there throws NG0951. - **Read reactively.** Use the signal in the template, inside `computed()`, or inside `effect()`. These rerun when results arrive or change, so you never need to guess the exact moment. - **Required queries** (`viewChild.required()`, `contentChild.required()`) throw NG0951 if read before results are collected or when nothing matches. The error page's advice is to read content results in `ngAfterContentChecked` and view results in `ngAfterViewChecked`, or later. - **No `static` option.** A signal query is always dynamic, so it can match nodes inside `@if` and `@for`. ## Content before view Content queries resolve earlier than view queries. A component's content is declared in its parent's template, and it is processed before the component's own view is checked. That is why the decorator hooks run `ngAfterContentInit` before `ngAfterViewInit`, and why a component can safely use its content query results while its own view is being checked. ## A migration that removes the timing question ```ts import {Component, ElementRef, computed, input, viewChild} from '@angular/core'; @Component({ selector: 'app-report', template: ` @if (ready()) { <canvas #plot></canvas> } <p>Canvas width: {{ width() }}</p> `, }) export class Report { ready = input(false); plot = viewChild<ElementRef<HTMLCanvasElement>>('plot'); width = computed(() => this.plot()?.nativeElement.width ?? 0); } ``` When `ready` flips to `true`, the canvas is created, the query signal changes, and `width` recomputes. There is no hook to pick, and no `static` flag to get wrong. ## One check, in order For a component with both kinds of decorator query, a single first check proceeds roughly like this: 1. Static queries were already resolved when the views were created, so they are set before `ngOnInit`. 2. `ngOnInit` and `ngDoCheck` run. 3. The component's content, declared in the parent's template, is processed; dynamic content queries are resolved and `ngAfterContentInit` runs. 4. The component's own view is checked, including `@if` and `@for` blocks; dynamic view queries are resolved and `ngAfterViewInit` runs. Signal queries sit outside this list: they do not wait for a hook, they simply report the current results whenever they are read after collection, and notify when those results change. ## Common mistakes 1. Adding `static: true` to silence an `ngOnInit` error on a node inside `@if`: it silences nothing, and the result never updates. 2. Reading a decorator query once in `ngAfterViewInit` and caching it, then being surprised when an `@if` swaps the node. 3. Writing a query result into a template-bound field from `ngAfterViewInit`, which changes a binding after it was checked and triggers the dev-mode ExpressionChangedAfterItHasBeenChecked error (NG0100). 4. Treating a signal query like a plain property and reading it once in the constructor. Doing DOM work after rendering, such as measuring or focusing, is a lifecycle concern of its own; the point here is only when a query has a value to give you.

  • When is static: true on @ViewChild a sound choice?
    When the queried node is always rendered, not inside `@if`, `@for` or `@defer`, and you need it in `ngOnInit`, for example a container element you hand to a third-party widget during initialisation. Because the result never updates, it is wrong for anything conditional.
  • What happens if a component reads viewChild.required() in its constructor?
    It throws NG0951, the required-query error: queries are never available in a constructor. Read it in the template, a `computed()` or `effect()`, or in `ngAfterViewChecked` and later.
  • Why can setting a bound field from ngAfterViewInit using a query result raise NG0100?
    The parent's bindings were already checked in that pass; changing a bound value afterwards makes the dev-mode verification pass see a different value, which is the ExpressionChangedAfterItHasBeenChecked error. Deriving the value with `computed()` from the query signal avoids writing after the check.

A dynamic query is a roll call taken after everyone has come in, including latecomers; a static query is a list written at the door, early but never updated. A signal query is a live headcount board that changes as people enter and leave.

saying these in an interview costs you the question

  • @ViewChild results are available in ngOnInit by default
  • static: true makes a query inside @if resolve earlier
  • Signal queries accept static: true like the decorators
  • Query results are available in the constructor once inject() has run
  • A static query keeps updating as the view changes