In Angular, how does a @ViewChildren QueryList report result changes, and what replaces that with the viewChildren() signal query?
answer
- changes is an Observable
- emits after, not the current set
- distinct changes only
- a readonly array in a signal
basics
~20 s@ViewChildren and @ContentChildren fill a QueryList whose changes Observable emits when the matches change, not the initial set. viewChildren() and contentChildren() instead return a signal of a readonly array that notifies computed() and effect() dependants.
solid answer
~40 sThe plural decorators assign a `QueryList<T>`: an iterable, array-like object with `length`, `first`, `last`, `get()`, `map()`, `filter()`, `find()`, `forEach()`, `reduce()`, `some()` and `toArray()`. To react to additions and removals you subscribe to `changes`, an `Observable` that emits the `QueryList` itself when results change; it does not replay the current set, so code often starts with the current value or pipes `startWith`. By default it emits only when the results actually changed (the `emitDistinctChangesOnly` option, which defaults to `true` and is deprecated). The signal functions `viewChildren()` and `contentChildren()` return a `Signal<ReadonlyArray<T>>`: read it in the template, `computed()` or `effect()`, and dependants update when the matches change. No subscription or teardown is needed.
go deeper
Remember that the plural decorators give a QueryList and the signal functions give a signal of an array.
Explain how changes behaves, including no replay of the initial set, distinct-only emission and the teardown you owe.
Replace changes subscriptions with computed or effect over signal queries during migrations, and spot leaked or late subscriptions in older code.
Plan the migration of a large codebase's decorator queries with the schematic, and decide where Observable-based consumers still justify QueryList.
## Two shapes for multi-result queries Angular offers two ways to collect **all** matching children: - The decorators `@ViewChildren` and `@ContentChildren`, which assign a **`QueryList<T>`** to a class property. - The signal functions `viewChildren()` and `contentChildren()`, which return a **`Signal<ReadonlyArray<T>>`**. Both follow the same matching rules (locator, `read`, and for content queries `descendants`). They differ in how you consume results and learn about changes. ## What a QueryList offers `QueryList` is array-like and iterable, so it works in `for...of` and in template loops. Its members include: - `length`, `first`, `last` and `get(index)` - `map()`, `filter()`, `find()`, `reduce()`, `forEach()` and `some()` - `toArray()` for a plain array copy - **`changes`**, an `Observable` that emits the `QueryList` when results change The list is populated before `ngAfterViewInit` (view queries) or `ngAfterContentInit` (content queries), and Angular updates it in place as `@if` and `@for` add or remove matches. ## How changes behaves 1. It is backed by a subject, so a subscriber sees only **later** changes; there is no replay of the results that exist when you subscribe. 2. It emits only when the set of results actually changed. That is governed by `emitDistinctChangesOnly`, which defaults to `true`; the option is marked deprecated and will be fixed at `true`. 3. The subscription is yours to end. A long-lived component that subscribes must tear it down when destroyed. A common decorator pattern therefore handles the initial set and later changes separately: ```ts import {AfterViewInit, Component, DestroyRef, QueryList, ViewChildren, inject} from '@angular/core'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {startWith} from 'rxjs'; import {Step} from './step'; @Component({selector: 'app-wizard', imports: [Step], template: `...`}) export class Wizard implements AfterViewInit { @ViewChildren(Step) steps!: QueryList<Step>; private destroyRef = inject(DestroyRef); ngAfterViewInit() { this.steps.changes .pipe(startWith(this.steps), takeUntilDestroyed(this.destroyRef)) .subscribe((list: QueryList<Step>) => console.log('steps', list.length)); } } ``` ## The signal version ```ts import {Component, computed, viewChildren} from '@angular/core'; import {Step} from './step'; @Component({selector: 'app-wizard', imports: [Step], template: `...`}) export class Wizard { steps = viewChildren(Step); count = computed(() => this.steps().length); } ``` The signal starts as an empty array, becomes the current matches once they are collected, and notifies dependants when they change. The array is read-only, and in Angular 22.2 the same array reference is returned while a check leaves the results unchanged, so a `computed()` over it does not rerun for nothing. ## Side by side | Aspect | `QueryList` (decorators) | `Signal<ReadonlyArray>` (functions) | |---|---|---| | Initial value | populated before the `After...Init` hook | empty array, then current matches | | Reacting to change | subscribe to `changes` | read in `computed()`, `effect()` or template | | Initial set in the stream | not replayed | always the current value when read | | Teardown | you unsubscribe | none | | Distinct-only emission | `emitDistinctChangesOnly`, default `true` | notifies only when results changed | | Container | array-like `QueryList` | plain read-only array | ## When each still matters - **Existing code** is full of `QueryList` and `changes` subscriptions, so interviewers still ask how they work. - **New code** in Angular 22.2 should prefer the signal functions: they compose with `computed()`, need no teardown, and have no separate initial-versus-later handling. - The `signal-queries-migration` schematic (`ng generate @angular/core:signal-queries-migration`) converts decorator queries and their references. ## Consuming the results in a template Both forms can drive markup. A `QueryList` is iterable, so `@for (s of steps; track s)` works over the decorator property once it is set. The signal form is called: `@for (s of steps(); track s)`. The difference shows up in derived values. With a `QueryList`, a count or a filtered subset shown in the template has to be recomputed by hand in a `changes` handler and stored in a field. With a signal query, a `computed()` such as `visibleSteps = computed(() => this.steps().filter(s => s.visible()))` stays correct by construction, because it reruns when either the matches or the signals it reads change. ## Pitfalls - Subscribing to `changes` in the constructor: the property is not assigned yet. - Forgetting that `changes` skips the initial set and wondering why nothing logs until the list changes. - Mutating the array returned by a signal query: it is read-only by type, and the array belongs to the query.
- Why do many QueryList subscriptions pipe startWith before subscribing?`changes` does not replay the current results; it only emits on later changes. `startWith(this.list)` makes the stream begin with the set that already exists, so one handler covers both the initial and the changed results.
- What does emitDistinctChangesOnly control?Whether `QueryList.changes` emits only when the results really changed. It defaults to `true`; setting it to `false` lets the stream emit even when nothing changed. The option is deprecated and will be fixed at `true`.
saying these in an interview costs you the question
- QueryList.changes emits the current results as soon as you subscribe
- viewChildren() returns a QueryList you subscribe to
- Signal query arrays should be mutated to reorder children
- A changes subscription needs no teardown in a long-lived component
- emitDistinctChangesOnly defaults to false