A view query gives a component a reference to something in its own template: an input to focus, a canvas to draw on, a child component to call. The API is easy. The timing is where bugs come from, because a query can only find an element after Angular has created it, and Angular creates a template’s elements in stages.
The classic interview question is “why is my @ViewChild undefined in ngOnInit?” The modern version adds signals: “when does viewChild() have a value, and how do you react when it changes?” This note answers both with one search component, compiled with the Angular 20.3 AOT compiler (ngc) and run under TestBed in Node. The log below is that run’s actual output. Angular 19 has the same APIs and timing.
The component under test
@Component({
selector: 'app-search',
template: `
<input #box />
@if (showHelp()) { <p #help>Type to search</p> }
`,
})
export class Search implements OnInit, AfterViewInit {
// signal queries (Angular 17.2+, stable since 19)
box = viewChild<ElementRef<HTMLInputElement>>('box');
help = viewChild<ElementRef<HTMLElement>>('help');
boxReq = viewChild.required<ElementRef<HTMLInputElement>>('box');
hasHelp = computed(() => !!this.help());
// decorator queries, for comparison
@ViewChild('box', { static: true }) staticBox?: ElementRef;
@ViewChild('box') dynBox?: ElementRef;
@ViewChild('help') helpDec?: ElementRef;
showHelp = signal(false);
constructor() {
console.log(`constructor: box=${!!this.box()} staticBox=${!!this.staticBox}`);
effect(() => console.log(`effect: box=${!!this.box()} help=${!!this.help()}`));
afterNextRender(() => console.log(`afterNextRender: box=${!!this.box()}`));
}
ngOnInit() { /* logs box, staticBox, dynBox */ }
ngAfterViewInit() { /* logs box, dynBox, help */ }
}There are two kinds of element in that template. The input sits directly in the component’s view. The p lives inside an @if block, which is an embedded view Angular creates only when the condition is true. That difference drives most of what follows.
What each stage sees
constructor: box=false staticBox=false
--- first detectChanges
ngOnInit: box=true staticBox=true dynBox=false
effect: box=true help=false
ngAfterViewInit: box=true dynBox=true help=false
afterNextRender: box=true
--- showHelp=true
immediately after set: help=false helpDec=false
effect: box=true help=true
after detectChanges: help=true hasHelp=true helpDec=trueWalk through it line by line.
Constructor: nothing. The class is instantiated before its template is. There’s no input element anywhere yet, so every query is empty. Any code that touches a query in the constructor is wrong, whichever API you use.
ngOnInit: it depends. By the time ngOnInit runs, Angular has run the template’s creation pass, so the input exists. The signal query box already returns it, and so does the decorator query marked static: true. The plain @ViewChild('box') (which defaults to static: false) is still undefined. Decorator queries without static: true are only assigned after the first update pass of the view, right before ngAfterViewInit.
That’s the full answer to the classic question. static: true says “this element is never inside a conditional or loop, so resolve it early”. It’s an instruction you give the compiler, and if you set it on something inside an @if, the query simply stays empty.
ngAfterViewInit: everything that exists. The decorator query is now set. But help is still false, because showHelp() is false and the p was never created. ngAfterViewInit runs once. If the element appears later, this hook won’t tell you.
afterNextRender: after the DOM is painted. This is where you put work that needs real layout, like measuring the input or handing a canvas to a chart library. It runs once, after the first render, and only in the browser (not during server-side rendering).
When an element appears later
The second half of the log flips showHelp to true. Immediately after set(), neither query has the paragraph, because Angular hasn’t run change detection yet and the @if block hasn’t been created. After the next detectChanges():
- the signal query
helpreturns the element, - the
computedbuilt on it updates to true, - the effect reading it re-runs on its own,
- and the decorator query
helpDechas the value too, but nothing notified you.
This is the real advantage of signal queries. A decorator query is a field that Angular overwrites; to react to changes you’d subscribe to a QueryList.changes observable (for @ViewChildren) or poll in ngAfterViewChecked. A signal query is a signal, so anything derived from it stays correct automatically.
// Focus the help text when it appears, without lifecycle hooks:
constructor() {
effect(() => this.help()?.nativeElement.focus());
}required queries and the compiler
viewChild.required() returns a signal typed without undefined, which removes a lot of ?. noise for elements that are always there. The cost is that reading it too early throws at runtime. Angular’s compiler tries to catch the obvious case. Reading a required query in the constructor didn’t even compile:
error NG8118: `boxReq` is a required `viewChild` and does not have a value in this context.Reading the same required query in ngOnInit compiled and worked, because the input is not inside a block. If the element were inside @if, required would be the wrong choice: the honest type is T | undefined.
Mistakes that show up in reviews
Calling detectChanges() in ngAfterViewInit to “fix” a value. Code reads a decorator query in ngAfterViewInit, sets a field the template binds to, and gets the error from the ExpressionChangedAfterItHasBeenCheckedError guide. The view was already checked with the old value. The fix is to derive the value (a computed over a signal query) or to restructure so the parent doesn’t depend on a child’s DOM after the fact.
Using setTimeout to wait for an element. It “works” because by the time the timer fires, change detection has run. With a signal query you don’t need to guess: an effect or computed runs when the element exists.
Confusing view and content queries. viewChild finds things in the component’s own template. Content passed in by the parent between the component’s tags is found with contentChild. A query that’s always undefined is often the wrong one of the two; the note on content projection covers the content side.
Using read without realising it. viewChild('box') on a plain element gives ElementRef. On a component, it gives the component instance. If you need the element of a component, or a directive on it, pass { read: ElementRef } or { read: SomeDirective }.
Interview practice
A colleague’s component shows a chart inside @if (data()) { <canvas #chart></canvas> }. They create the chart in ngAfterViewInit with this.chartRef.nativeElement, using @ViewChild('chart') chartRef!: ElementRef. It crashes with Cannot read properties of undefined (reading 'nativeElement') whenever the data loads after the first render. Explain and fix it.
Answer
ngAfterViewInit runs once, after the first render. At that moment data() is still empty, so the @if block hasn’t been created and the canvas doesn’t exist. The decorator query is undefined, and the ! in the type told TypeScript to stop warning about exactly that. When the data arrives later, the canvas appears, but ngAfterViewInit never runs again.
I’d switch to a signal query and react to it: canvas = viewChild<ElementRef<HTMLCanvasElement>>('chart'), then in the constructor an effect that reads both this.canvas() and this.data(), returns early if either is missing, creates the chart once, and updates it on later data changes. The effect’s onCleanup (or DestroyRef.onDestroy) destroys the chart instance. If the chart library measures layout on creation, I’d create it with afterRenderEffect so it runs after the DOM is painted rather than during change detection. And I’d remove the !, because the element is genuinely optional; viewChild.required would be wrong here for the same reason.