Don’t Use @ViewChild/Children Decorators! Use Signals Instead

November 22, 2024 | 10 Minute Read

Hey there Angular folks, and welcome back! In this tutorial, we’re tackling an exciting update in Angular: how to modernize your components by migrating from the traditional @ContentChild and @ContentChildren decorators, and QueryList, to the new signal-based contentChild and contentChildren functions.

We’ll explore how these new features work, why they’re helpful, and walk you through a couple of examples to help it all make sense.

Now, recently I created another tutorial where we converted the @ViewChild and @ViewChildren decorators over to signals, so this tutorial will look a little bit familiar, but this time we’ll be dealing with a component’s “projected content” instead its “view” a.k.a. the template.

Ok, let’s get started!

The Starting Point: Our Current Example

Like several of my other tutorials, we’ll be using a demo app that lists out several NBA players:

Example Angular application

It has a search field that can be used to filter the list of players and, when the app is initialized, the search field is automatically focused:

Example of the search textbox focused on initialization using the @ContentChild decorator

Now, we’ll see how in a minute, but this programmatic focus is currently handled with the @ContentChild decorator.

The data for each of the players in this list is formatted with a “player component”.

Currently we are using the @ContentChildren decorator and the QueryList class to provide the total count of player components visible in the list and we are displaying this count right under the search field:

Example of the player components count displayed using the @ContentChildren decorator

Probably not exactly something that you would want to do in the real-world but it should work just fine for the purposes of this demo.

So that’s what we’re starting with, now let’s convert these both over to the new signals-based method instead.

Let’s start with the programmatic focus of the search field.

Step 1: Converting the @ContentChild Decorator to the contentChild() Function

To begin, let’s open the app component template.

Here’s the search field input element:

<app-search-layout>
    <ng-container search-form>
        <label for="search">Search Players</label>
        <input 
            #searchField 
            type="search" 
            id="search" 
            autocomplete="off" 
            [(ngModel)]="searchText" 
            [formControl]="search" 
            placeholder="Search by entering a player name" />
    </ng-container>
    ...
</app-search-layout>

We can see that this input is placed within a parent search-layout.component.

Then, within this component we have two slots, one for the “search-form”, and another for the “search-list”.

<app-search-layout>
    <ng-container search-form>
      ...
    </ng-container>
    <ng-container search-list>
      ...
    </ng-container>
</app-search-layout>

This means these items are within the “content” of the search-layout.component as opposed to the “view”.

Also important to note here, we have a “searchField” template reference variable that we use to get a handle to this input element when using the @ContentChild decorator:

<input #searchField ... />

Ok, so how and where is the focus getting applied?

Well, this is happening in our search-layout.component.

Here, we’re using the @ContentChild decorator to query for the “searchField” template reference variable:

@ContentChild('searchField') private searchField?: ElementRef<HTMLInputElement>;

Then, we use the AfterContentInit lifecycle hook to access the native element and set programmatic focus:

ngAfterContentInit() {
    this.searchField?.nativeElement.focus();
}

Let’s switch this concept and update it to use signals.

First, we can remove the decorator. Then we’ll set this property to a signal using the new contentChild() function.

We need to be sure to import this function from the @angular/core module too.

This contentChild() signal will be typed to our existing ElementRef, HTMLInputElement.

Then, we just need to add our reference variable “searchField” as the “locator”:

import { ..., contentChild } from '@angular/core';

private searchField = contentChild<ElementRef<HTMLInputElement>>('searchField');

And that’s it, this is now a signal.

So now that it’s a signal, we can switch away from the AfterContentInit lifecycle function.

Instead, we can use the effect() function which allows us to react to signal value changes.

And we’ll need to be sure that it gets imported from the @angular/core module as well.

Within this function we can move our programmatic focus and update it to use the new signal property by adding parenthesis:

import { ..., effect } from '@angular/core';

constructor() {
    effect(() => {
        this.searchField()?.nativeElement.focus();
    })
}

That’s it.

Now, once we save, we’d see that the search field gets focused when initialized just like we want.

So, it will work exactly like it used to, but instead of using the old decorator, it’s now done using a more modern signals-based approach.

Step 2: Converting the @ContentChildren Decorator to the contentChildren() Function

Ok, now let’s update the player components count concept.

First let’s understand how it’s working currently.

Let’s switch back to the app component template.

Ok, here in the “search-list” slot region for the search-layout.component, we have a for loop that loops out a list of player components:

<ng-container search-list>
    @for (player of filteredPlayers(); track player.name) {
        <app-player [player]="player"></app-player>
    } @empty {
        <p>Sorry, we couldn't find any players with the name you entered</p>
    }
  </ng-container>

So, these are also within the “content” of the search-layout.component.

But in this case there are potentially multiple of these components depending on how the list is filtered, so we are using the @ContentChildren decorator for this instead of the @ContentChild decorator.

Let’s switch back to the search-layout.component TypeScript to see how this is being set.

Ok, here we have the a “playerComponents” private field that’s set using the @ContentChildren decorator and the QueryList class to query for all “player components”:

@ContentChildren(PlayerComponent) private playerComponents?: QueryList<PlayerComponent>;

We also have this “playerComponentsCount” property:

protected playerComponentsCount: number = 0;

To set this property, we have this “updatePlayerComponentsCount()” function:

private updatePlayerComponentsCount() {
    this.playerComponentsCount = this.playerComponents?.length ?? 0;
    this.playerComponents?.changes.pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(components => this.playerComponentsCount = components.length);
}

This function uses an observable subscription to the “playerComponents” QueryList “changes” observable to update the count.

So, every time the QueryList is updated, the count will be updated to the length of the list.

Then, if we switch over to the template for this component, we can see that we’re simply rendering the value of this property with string interpolation:

<p>Player Count: {{ playerComponentsCount }}</p>

Ok, now let’s convert this to signals.

First, let’s remove the @ContentChildren decorator and QueryList class.

Then we’ll set this signal with the new contentChildren() function.

And, we’ll need to be sure to import this from @angular/core as well.

Then, we need to pass it the “PlayerComponent” as its locator:

import { ..., contentChildren } from '@angular/core';

private playerComponents = contentChildren(PlayerComponent);

Ok, this is now a signal, so we can update how the count gets set.

And this is my favorite part of the demo because we can simplify things quite a bit.

Rather than use the function with the observable subscription, we can use a new concept where we create a signal from another signal with the computed() function.

Just like the others, we need to import it from @angular/core too:

import { ..., computed } from '@angular/core';

protected playerComponentsCount = computed(() => {});

Now, anytime the signals added within this function change, this signal value will be updated too, based on the values of the associated signals.

Since our player components list is now a signal, all we need to do is return its length in this computed function:

protected playerComponentsCount = computed(() => this.playerComponents().length);

This means that we can remove the old function, the AfterContentInit function and its imports, the takeUntilDestroyed function and its imports, and the DestroyRef too.

Then, we just need to switch to the template and add parenthesis to the usage of this counts property:

<em>Player Components Visible: {{ playerComponentsCount() }}</em>

And that’s it.

When we save now it should all be working as expected.

It should have the correct count to start and then as we filter the list, the value should update just like we want it to.

So, it should work just like it did before, but now instead of using the old @ContentChildren decorator and QueryList class, it’s done using a more modern, signals-based approach.

In Conclusion

So, switching from the old content decorators to these new signal queries is pretty easy and makes things simpler in many cases.

Ok, hopefully that was helpful.

Don’t forget to check out my other Angular tutorials for more tips and tricks.

Additional Resources

Want to See It in Action?

Check out the demo code and examples of these techniques in the in the Stackblitz example below. If you have any questions or thoughts, don’t hesitate to leave a comment.