Queries give you access to elements, components or directives from the template. Since v19 they are signals: viewChild() searches in the component’s own template, contentChild() searches in the projected content from the parent (what is placed between the component’s tags). Because they are signals, you can use them directly in computed() and effect(), without ngAfterViewInit and without QueryList.changes.
| Searches in | Result | |
|---|---|---|
viewChild | the component’s template | Signal<T | undefined> |
viewChildren | the component’s template | Signal<readonly T[]> |
contentChild | the projected content (ng-content) | Signal<T | undefined> |
contentChildren | the projected content | Signal<readonly T[]> |
viewChild and viewChildren
import { Component, ElementRef, viewChild, viewChildren, afterNextRender } from '@angular/core';
@Component({
selector: 'app-search-box',
template: `
<input #search />
<app-results />
@for (item of items(); track item.id) { <li #row>{{ item.name }}</li> }
`,
})
export class SearchBox {
// by template reference variable
private readonly search = viewChild.required<ElementRef<HTMLInputElement>>('search');
// by the component type
private readonly results = viewChild(Results); // Signal<Results | undefined>
// all the matches
private readonly rows = viewChildren<ElementRef<HTMLLIElement>>('row'); // Signal<readonly ElementRef[]>
constructor() {
// ✗ In the constructor the view does not exist yet: this.search() would throw (required)
afterNextRender(() => this.search().nativeElement.focus()); // ✓ after the first render
}
protected readonly rowCount = computed(() => this.rows().length); // ✓ reactive
}The read option
// `read` chooses WHAT you get from the element found
readonly input = viewChild('field', { read: ElementRef }); // the DOM element
readonly model = viewChild('field', { read: NgModel }); // a directive on it
readonly host = viewChild('slot', { read: ViewContainerRef }); // for dynamic components
readonly tpl = viewChild('tpl', { read: TemplateRef }); // an <ng-template>
readonly inj = viewChild(Child, { read: Injector }); // v22.2: the child’s injectorcontentChildren: components that collaborate
The classic pattern: a container (Tabs) and declarative children (Tab). The parent writes them in HTML, the container discovers them with contentChildren and decides how to display them.
// Tabs + Tab: the container discovers the children that the PARENT placed
@Component({
selector: 'app-tab',
template: `<ng-template #content><ng-content /></ng-template>`,
})
export class Tab {
readonly title = input.required<string>();
readonly content = viewChild.required<TemplateRef<unknown>>('content');
}
@Component({
selector: 'app-tabs',
imports: [NgTemplateOutlet],
template: `
@for (tab of tabs(); track tab; let i = $index) {
<button [class.active]="i === active()" (click)="active.set(i)">{{ tab.title() }}</button>
}
@if (current(); as tab) {
<ng-container [ngTemplateOutlet]="tab.content()" />
}
`,
})
export class Tabs {
protected readonly tabs = contentChildren(Tab); // Signal<readonly Tab[]>
protected readonly active = signal(0);
protected readonly current = computed(() => this.tabs()[this.active()]);
}The user’s name, email and avatar.
descendants and contentChild
// contentChildren by default only looks at the DIRECT children of the projected content.
// With descendants: true it searches at any depth (e.g. inside a <div> wrapper).
readonly items = contentChildren(MenuItem, { descendants: true });
// contentChild for a single element (e.g. an optional template for a header)
readonly headerTpl = contentChild<TemplateRef<unknown>>('header');Before: decorators and QueryList
// ✗ The old API
@ViewChild('search') search!: ElementRef<HTMLInputElement>;
@ViewChildren(RowComponent) rows!: QueryList<RowComponent>;
@ContentChildren(Tab) tabs!: QueryList<Tab>;
ngAfterViewInit() {
this.search.nativeElement.focus();
this.rows.changes.subscribe(() => this.updateCount()); // manual subscription to changes
}For content passed in from outside, see Content projection; for working with the DOM after rendering, afterNextRender in Lifecycle & DestroyRef.
When YES, when NO
- Focus, scroll, DOM measurements (with
afterNextRender). - Integration with libraries that need an element (charts, editors).
- Compound components: Tabs/Tab, Menu/MenuItem, Accordion/Panel.
- Getting a
ViewContainerReffor dynamic components.
- To read/write a child’s state → use input/output/model.
- To call methods on a child for things that can be declarative (e.g. “child.refresh()” on every change) → pass an input.
- To change classes/styles → bindings in the template.
Common pitfalls
- Reading too early: in the constructor the queries have no value; a
.requiredthrowsNG0951. - An element under
@if:viewChildbecomesundefinedwhen the block is hidden — don’t use.requiredthere. - DOM measurements in
computed(as in the demo) work, but they are not reactive to resizes — for that you need aResizeObserver.
Cheat sheet
viewChild('ref') // Signal<T | undefined>
viewChild.required('ref') // Signal<T> (throws if read before it exists)
viewChild(MyComp) // by type
viewChildren('ref' | MyComp) // Signal<readonly T[]>
contentChild(...) / contentChildren(..., { descendants })
{ read: ElementRef | TemplateRef | ViewContainerRef | Directive | Injector }
// migration: ng generate @angular/core:signal-queries