Signal-Based Queries
Angular’s signal-based queries replace the old decorator-driven @ViewChild/@ContentChild API with reactive functions that return signals. Instead of relying on lifecycle hooks like ngAfterViewInit to know when a query result is available, you simply read a signal and Angular guarantees the value is current. This makes queries composable with computed() and effect(), eliminates timing bugs, and gives you precise typing for the presence or absence of a match.
Why signal queries
Decorator queries had a subtle but persistent problem: the queried element wasn’t populated until a specific lifecycle moment, and reading it too early gave you undefined. Signal queries solve this by exposing the result as a signal whose value is always consistent with the rendered DOM. You can read it inside an effect, derive other signals from it, and never worry about whether the view has initialized.
The functions come in four flavors covering single vs. multiple results, and view vs. content projection:
| Function | Returns | Scope |
|---|---|---|
viewChild() | Signal<T | undefined> | This component’s own template |
viewChildren() | Signal<readonly T[]> | This component’s own template |
contentChild() | Signal<T | undefined> | Projected <ng-content> |
contentChildren() | Signal<readonly T[]> | Projected <ng-content> |
Querying view children
viewChild() locates a single element or directive within the component’s own template. You pass either a template reference variable name or a component/directive class as the locator.
import { Component, viewChild, ElementRef, effect } from '@angular/core';
@Component({
selector: 'app-search',
standalone: true,
template: `
<input #box type="text" placeholder="Search..." />
<button (click)="focus()">Focus input</button>
`,
})
export class SearchComponent {
// Read by template reference variable
private box = viewChild<ElementRef<HTMLInputElement>>('box');
constructor() {
effect(() => {
const el = this.box();
if (el) {
console.log('Input is ready:', el.nativeElement.tagName);
}
});
}
focus(): void {
this.box()?.nativeElement.focus();
}
}
Output:
Input is ready: INPUT
For multiple matches, viewChildren() returns a signal holding a read-only array. The array updates reactively as @for-rendered items are added or removed.
import { Component, viewChildren, ElementRef, computed, signal } from '@angular/core';
@Component({
selector: 'app-tabs',
standalone: true,
template: `
@for (tab of tabs(); track tab) {
<div #panel class="panel">{{ tab }}</div>
}
<p>Rendered panels: {{ panelCount() }}</p>
`,
})
export class TabsComponent {
tabs = signal(['Home', 'Profile', 'Settings']);
panels = viewChildren<ElementRef<HTMLDivElement>>('panel');
panelCount = computed(() => this.panels().length);
}
Output:
Rendered panels: 3
Querying content children
When a component projects content through <ng-content>, use contentChild() and contentChildren() to query the projected nodes. These match elements supplied by the parent, not elements declared in the component’s own template.
import { Component, contentChildren, Directive, input } from '@angular/core';
@Directive({ selector: '[appItem]', standalone: true })
export class ItemDirective {
label = input.required<string>({ alias: 'appItem' });
}
@Component({
selector: 'app-list',
standalone: true,
template: `<ng-content />`,
})
export class ListComponent {
items = contentChildren(ItemDirective);
labels(): string[] {
return this.items().map((i) => i.label());
}
}
Used by a parent:
<app-list>
<span appItem="First">First</span>
<span appItem="Second">Second</span>
</app-list>
By default,
contentChild/contentChildrenquery only direct projected children. Pass{ descendants: true }to also match elements nested deeper inside the projected content.
Required queries
When a queried element is guaranteed to exist, use the .required variant. This narrows the return type from Signal<T | undefined> to Signal<T>, removing the need for null checks. If the element is missing at runtime, Angular throws a clear error instead of silently returning undefined.
import { Component, viewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-canvas',
standalone: true,
template: `<canvas #surface width="320" height="180"></canvas>`,
})
export class CanvasComponent implements AfterViewInit {
// No `| undefined` — type is ElementRef<HTMLCanvasElement>
private surface = viewChild.required<ElementRef<HTMLCanvasElement>>('surface');
ngAfterViewInit(): void {
const ctx = this.surface().nativeElement.getContext('2d')!;
ctx.fillStyle = '#2563eb';
ctx.fillRect(10, 10, 120, 60);
}
}
Note that required exists only for the single-result functions. viewChildren() and contentChildren() already return an array (empty when nothing matches), so a required variant would be meaningless.
Query options
All four functions accept an options object as the final argument.
| Option | Applies to | Purpose |
|---|---|---|
read | all | Read a specific token (e.g. ElementRef, ViewContainerRef, a provider) from the matched node instead of the default. |
descendants | contentChild/contentChildren | When true, includes content nested below the first level of projection. |
import { Component, viewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-host',
standalone: true,
template: `<div #slot></div>`,
})
export class HostComponent {
// Read a ViewContainerRef instead of ElementRef for dynamic rendering
slot = viewChild('slot', { read: ViewContainerRef });
}
Best practices
- Prefer signal queries over
@ViewChild/@ContentChilddecorators in new code; they are reactive and eliminate lifecycle timing pitfalls. - Use the
.requiredvariant whenever the element is statically guaranteed to be present to drop unnecessary null checks. - Always provide a precise generic type (e.g.
ElementRef<HTMLInputElement>) so consumers get full type safety. - Combine query signals with
computed()to derive state declaratively rather than reading them imperatively in hooks. - Reach for
read: ViewContainerRef(or another token) instead of grabbing the DOM element when you need a specialized Angular API. - Avoid mutating queried DOM directly inside an
effectduring initial render; preferafterRenderEffectorngAfterViewInitfor one-time setup that touches the DOM.