Skip to content

Signals: viewChildren() gets new values even when query list is unchanged, and performance is hampered by lack of a 'static' option #54376

@simeyla

Description

@simeyla

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

Traditionally only @ViewChild and @ContentChild has had a static option.
@ViewChildren and @ContentChildren do not, however there isn't really a good reason given. The best information I could find was:

@ViewChildren and @ContentChildren queries are already "dynamic" by default and don't support static resolution.

Traditionally the static option is useful to developers because it allows for the value of @ViewChild to be consumed in ngOnInit. There is also an additional performance benefit too since it only ever gets calculated once, even if for most components this would be negligible.

There is another (deprecated) option 'emitDistinctChangesOnly' for @ViewChildren](https://angular.io/api/core/ViewChildren) intended to only emit a new if the QueryList has actually changed. I haven't looked into the details of this, but I'm assuming it does some kind of identity comparison for items in the QueryList.

When playing with the new viewChildren() signal in Angular 17.2 I found that the signal yields duplicate values even if the list has not changed. You can get an infinite loop if this value is used in certain ways in the template because Angular keeps recalculating it:

Error: NG0103: Infinite change detection while trying to refresh views.

In the attached example my app component behaves as a custom grid control. There are a number of <ng-template column-template></ng-template> columns defined, that are captured to a signal via viewChildren(ColumnTemplateDirective). In my real project the component uses mat-table but in this example I just output the list of columns.

The problem is that when stamping out the columns new views are created, which causes viewChildren to be recalculated. Even though the list of ColumnTemplateDirective is unchanged it 'emits' a new value causing the infinite loop.

Notes about my repro

Open stackblitz console to see the error

My reproduction of this issue can easily be 'fixed' and I am well aware of ways in which it could be.

  1. I could create a computed with a custom equals that takes the viewChildren list and ignores duplicate values.
  2. The @for in my repro that iterates through the columns computed signal uses object identity. If changed to track col.id then the problem goes away because the template inside doesn't keep getting recreated.

I don't know exactly what triggers viewChildren to recalculate itself, but I had to add an extra 'scope' with @if (true) in order to trigger it to do so.

Frankly it was harder to create this repro than I expected. It was much easier with mat-table to trigger the infinite loop, but I wanted to stay within core for the repro.

Important

Ultimately what I would hope to see is two things:

  1. Identity checking of elements returned from viewChildren() such that unchanged children do not update the signal value.
  2. A way to use something like static for viewChildren() in order to only calculate the list one time. I don't think it would make sense for contentChildren(). This would primarily be for performance reasons when needing to capture viewChildren that are known to be static. This was easier before with QueryList that was an observable.

I can easily work around the duplicate viewChildren value, but I am more concerned about performance especially since my component displays a grid with thousands of cells and Angular is probably continuously looking through the whole table for new templates.

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/angular-model-inputs-mv3bsn

Please provide the exception or error you saw

Error: NG0103: Infinite change detection while trying to refresh views.

Please provide the environment you discovered this bug in (run ng version)

Angular: 17.2.0-rc.1

Anything else?

No response

Metadata

Metadata

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions