Angular Signal Queries: viewChild, contentChild, viewChildren, contentChildren [2026]

Link copied
Angular Signal Queries: viewChild, contentChild, viewChildren, contentChildren [2026]

Angular Signal Queries: viewChild, contentChild, viewChildren, contentChildren [2026]

When a component needs a reference to one of its own template elements, a child component, or content projected into it, the answer is a query. Since Angular 17.2, queries are signalsviewChild(), contentChild(), and their plural cousins. The signal-based versions replaced the decorator-based @ViewChild/@ContentChild from the previous era and are now the standard.

This is lesson 2.13 of the Angular Tutorial. It assumes you understand content projection (lesson 2.6) and template reference variables (lesson 2.2).

The four query functions #

Four functions cover every case:

Function Returns What it queries
viewChild<T>() Signal<T | undefined> One element/component in this component's own template
viewChildren<T>() Signal<readonly T[]> Many elements/components in this component's own template
contentChild<T>() Signal<T | undefined> One element/component projected into this component (via <ng-content>)
contentChildren<T>() Signal<readonly T[]> Many projected elements/components

The singular returns one or undefined; the plural returns an array. All four are signals — read them with (), react to them in computed() or effect().

Querying by template-reference variable #

The most common case — reference an element you marked with #name:

import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';

@Component({
  selector: 'app-search-box',
  template: `
    <input #search type="search" />
    <button (click)="focusInput()">Focus</button>
  `,
})
export class SearchBoxComponent {
  searchInput = viewChild<ElementRef<HTMLInputElement>>('search');

  focusInput() {
    this.searchInput()?.nativeElement.focus();
  }
}

Three facts:

  1. 'search' is the template reference name. Matches #search in the template.
  2. The generic <ElementRef<HTMLInputElement>> types the return. Without it the signal is typed loosely.
  3. Read with searchInput() and check for undefined. The query returns undefined until the template renders.

The singular pattern is universal — declare with a string, type with a generic, read with () + null-check.

Querying by class #

For child components and directives, you can query by class instead of by template name:

import { Component, viewChild, viewChildren } from '@angular/core';
import { TabComponent } from './tab.component';

@Component({
  selector: 'app-tabs',
  imports: [TabComponent],
  template: `
    <app-tab #firstTab label="General" />
    <app-tab label="Privacy" />
    <app-tab label="Account" />
  `,
})
export class TabsComponent {
  firstTab = viewChild('firstTab');                  // by reference variable
  firstTabByClass = viewChild(TabComponent);          // by class — same component
  allTabs = viewChildren(TabComponent);               // all instances
}

Querying by class is the right approach when:

  • You have multiple instances of the same component
  • You want them all (use the plural)
  • You don't want to add #name to each one

The singular viewChild(TabComponent) returns the FIRST instance. The plural viewChildren(TabComponent) returns all of them, in template order.

required: true — query that must succeed #

A query that must always resolve can be marked required:

export class FormComponent {
  // Throws an error at runtime if the template doesn't have a #form element
  formEl = viewChild.required<ElementRef<HTMLFormElement>>('form');

  submit() {
    this.formEl().nativeElement.submit();   // no null-check needed — typed as ElementRef, not undefined
  }
}

The signal is typed as T, not T | undefined. Use this when the absence of the queried element is a programmer error, not a runtime possibility.

Reading by type from a directive — the read option #

When the same template element has multiple meaningful types (a component AND a directive AND an ElementRef), the read option picks which:

@Component({
  template: `<app-card appHoverGlow #ref>...</app-card>`,
})
export class HostComponent {
  // Default — gets the component instance
  card = viewChild(CardComponent);

  // Explicitly the directive instance
  glow = viewChild(CardComponent, { read: HoverGlowDirective });

  // Explicitly the host element ref
  el = viewChild('ref', { read: ElementRef });

  // Explicitly the template ref (for `<ng-template>`)
  tpl = viewChild('ref', { read: TemplateRef });
}

The read option disambiguates when there is more than one viable token. The default is the component class for class-queries, and ElementRef for template-name queries on regular elements.

When the query is populated #

This is the timing rule that catches everyone:

  • viewChild is populated after the view has rendered
  • contentChild is populated after content is projected

Reading the signal in the constructor returns undefined — the template hasn't rendered yet. Read it inside an effect() (reacts when populated), inside afterNextRender() (runs after first render), or in any event handler / template binding.

export class SearchBoxComponent {
  searchInput = viewChild<ElementRef<HTMLInputElement>>('search');

  constructor() {
    // ❌ undefined here — too early
    console.log(this.searchInput());

    // ✅ runs once the view is in the DOM
    afterNextRender(() => {
      this.searchInput()?.nativeElement.focus();
    });

    // ✅ reacts whenever the queried element changes
    effect(() => {
      const el = this.searchInput();
      if (el) console.log('Search input mounted', el.nativeElement);
    });
  }
}

contentChild for projected content #

A wrapping component can query content the parent passed in:

@Component({
  selector: 'app-modal',
  template: `
    <header>
      <ng-content select="[modal-title]" />
    </header>
    <main><ng-content /></main>
  `,
})
export class ModalComponent {
  // Query for the projected title element
  title = contentChild<ElementRef<HTMLElement>>('modalTitle');

  constructor() {
    effect(() => {
      const el = this.title();
      if (el) console.log('Modal title:', el.nativeElement.textContent);
    });
  }
}

// Consumer
<app-modal>
  <h2 modal-title #modalTitle>Confirm delete</h2>
  <p>Are you sure?</p>
</app-modal>

contentChild works the same way as viewChild — same required option, same read option, same generic typing. The only difference is where it looks: own template (view) vs projected content (content).

A tabs component using all four #

A realistic case using both the singular and plural of view and content queries:

import { Component, contentChildren, viewChild, signal, effect, ElementRef } from '@angular/core';

@Component({
  selector: 'app-tab',
  template: `
    @if (active()) {
      <ng-content />
    }
  `,
})
export class TabComponent {
  label = input.required<string>();
  active = signal(false);
}

@Component({
  selector: 'app-tabs',
  imports: [TabComponent],
  template: `
    <nav #tabBar>
      @for (t of tabs(); track t.label(); let i = $index) {
        <button (click)="activeIdx.set(i)">{{ t.label() }}</button>
      }
    </nav>
    <ng-content />
  `,
})
export class TabsComponent {
  tabs = contentChildren(TabComponent);                       // projected tabs
  tabBar = viewChild<ElementRef<HTMLElement>>('tabBar');      // own template ref
  activeIdx = signal(0);

  constructor() {
    effect(() => {
      const tabs = this.tabs();
      const idx = this.activeIdx();
      tabs.forEach((tab, i) => tab.active.set(i === idx));
    });
  }
}

contentChildren(TabComponent) grabs every projected <app-tab>. viewChild('tabBar') grabs the wrapping component's own nav element. The effect synchronizes the active state across all tabs whenever either signal changes.

Decorator-based queries — what changed #

The legacy pattern, still seen in older code:

export class TabsComponent implements AfterViewInit, AfterContentInit {
  @ViewChild('tabBar') tabBar!: ElementRef;
  @ContentChildren(TabComponent) tabs!: QueryList<TabComponent>;

  ngAfterViewInit() { /* use this.tabBar */ }
  ngAfterContentInit() { /* iterate this.tabs */ }
}

Differences from signal queries:

Aspect Decorator queries Signal queries
Result type T or QueryList<T> Signal<T> or Signal<T[]>
Initialization Set after lifecycle hook fires Reactive — fire effect() automatically
Lifecycle pairing Needs AfterViewInit / AfterContentInit No lifecycle hook needed
Required option Couldn't enforce at compile time viewChild.required<T>() enforces
Tree-shaking Decorator metadata included Pure functions, better tree-shaking

The Angular CLI ships a migration: ng generate @angular/core:signal-queries-migration. Run it on any v17.2+ codebase.

Common gotchas #

Symptom Cause Fix
Signal returns undefined in constructor Template hasn't rendered yet Move the read inside effect() or afterNextRender()
viewChild for <ng-template> returns ElementRef, not TemplateRef Default read is ElementRef; templates need explicit hint Add { read: TemplateRef }
viewChildren array is empty on first effect run @for items haven't been created yet The effect re-runs when the queried list changes — second run will have the items
contentChild empty even though projection works Used viewChild for projected content Switch to contentChild
Required query throws The queried element really is missing from the template Either ensure the element exists, or remove .required<>() and handle undefined

What's next #

Lesson 2.14 closes Module 2 with programmatic component creation — ViewContainerRef.createComponent() for modal/portal patterns and dynamic component plug-ins. After that, Module 3 dives into the signal system in depth.

Try it yourself #

Auto-focus the first input on mount, using contentChild to find it in projected content:

@Component({
  selector: 'app-form-card',
  template: `
    <section class="card">
      <ng-content />
    </section>
  `,
})
export class FormCardComponent {
  firstInput = contentChild<ElementRef<HTMLInputElement>>('firstInput');

  constructor() {
    afterNextRender(() => {
      this.firstInput()?.nativeElement.focus();
    });
  }
}

// Consumer
<app-form-card>
  <input #firstInput placeholder="Name" />
  <input placeholder="Email" />
</app-form-card>

The wrapping card auto-focuses whichever input the consumer marks with #firstInput. The card has no knowledge of what is projected — it just queries for the reference.

YouI need to read a child’s value RIGHT after construction. Why is my viewChild() undefined?
Claude · used get_best_practicesBecause the template hasn’t rendered yet. The constructor runs before the view exists. Move the read into afterNextRender(() => this.child()?.doThing()) for one-time access, or into effect(() => { const c = this.child(); if (c) ... }) for reactive access. The effect approach is best for queries that may change (a @for list, a conditional @if) — the effect re-fires when the query updates.

Lesson 2.14 picks up programmatic component creation — the dynamic-rendering escape hatch.

Up next in Angular

More from this topic

View all Angular articles →
Angular

When Angular is launched ?

Link copied When Angular is launched ? When Angular Landed: A History of the Popular Web Framework # Angular, …

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email stays private. Required fields are marked *

Leave a Comment

Your email stays private. Required fields are marked *