Skip to content
Angular ng signals 4 min read

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:

FunctionReturnsScope
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/contentChildren query 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.

OptionApplies toPurpose
readallRead a specific token (e.g. ElementRef, ViewContainerRef, a provider) from the matched node instead of the default.
descendantscontentChild/contentChildrenWhen 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/@ContentChild decorators in new code; they are reactive and eliminate lifecycle timing pitfalls.
  • Use the .required variant 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 effect during initial render; prefer afterRenderEffect or ngAfterViewInit for one-time setup that touches the DOM.
Last updated June 14, 2026
Was this helpful?