Skip to content

fix(core): components marked for traversal resets reactive context #61663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

kristilw
Copy link

@kristilw kristilw commented May 23, 2025

when marked for traversal the reactive context has to be set to null to avoid inheriting the reactive context of the parent component

PR Closes #61662

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.dev application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

Issue Number: #61662

What is the new behavior?

The embedded view updates its template when it depends on a signal which has changed its value.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

@angular-robot angular-robot bot added the area: core Issues related to the framework runtime label May 23, 2025
@ngbot ngbot bot added this to the Backlog milestone May 23, 2025
@kristilw kristilw force-pushed the bug-change-detection branch from 0662b7f to cbd8381 Compare May 26, 2025 08:04
@kristilw kristilw changed the title test(core): add test for change detction bug fix(core): components marked for traversal resets reactive context May 26, 2025
@kristilw kristilw force-pushed the bug-change-detection branch from cbd8381 to 5283e46 Compare May 26, 2025 08:09
@kristilw kristilw marked this pull request as ready for review May 26, 2025 08:09
@pullapprove pullapprove bot requested a review from thePunderWoman May 26, 2025 08:14
@thePunderWoman thePunderWoman requested review from atscott and removed request for thePunderWoman May 26, 2025 08:39
Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct in isolation.

(TMI disclaimer) I'm still not sure about how I feel about the attached view case in general. It wasn't the intent that attaching a view would skip refreshing the component. Skipping it will result in its queries not being updated. E.g. if I add a test like this:

     @Directive({
          selector: '[step]'
        })
        class Step {}
    
        @Directive({selector: '[dataDirective]'})
        class DataDirective {
          readonly templateRef = inject(TemplateRef);
          readonly viewContainerRef = inject(ViewContainerRef);
    
          drawTemplate() {
            this.viewContainerRef.createEmbeddedView(this.templateRef, {$implicit: data});
          }
        }
    
        @Component({
          selector: 'child',
          template: '<ng-container *dataDirective="let data"><div step> {{data()}}</div></ng-container>',
          imports: [DataDirective, Step],
          changeDetection: ChangeDetectionStrategy.OnPush,
        })
        class ChildComponent {
          @ViewChildren(Step) steps?: QueryList<Step>;
          signalSteps = viewChildren(DataDirective);
        }
        ...

The length of steps and signalSteps should really both be 1 after you call drawTemplate and wait for stability, but it's not because view queries are updated at the end of refreshing the component view:

const viewQuery = tView.viewQuery;
if (viewQuery !== null) {
executeViewQueryFn<T>(RenderFlags.Update, viewQuery, context);
}

This is one of the reasons we ensure that embedded views share consumers with their parent component and why the temporary consumer marks the component view for refresh. The "correct" thing to do for this case would be to ensure that the component gets refreshed when an embedded view is attached but that comes with its own difficulties. There are similar problems with the old style queries that aren't necessarily solvable and we can kind of shrug and say to use signal queries to fix those problems.

This issue already exists and again the change here is still correct and doesn't introduce any regression. But I think it's worth noting that this "fixes" the issue in a way that's not necessarily directly addressing the problem with the attached view not marking its parent OnPush component for check.

}
if (!isInCheckNoChangesPass) {
addAfterRenderSequencesForView(lView);
// Sets active consumer to null, ensuring that an improper reactive context is not inherited
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Sets active consumer to null, ensuring that an improper reactive context is not inherited
// Set active consumer to null to avoid inheriting an improper reactive context

optional: Slight restructure to active voice and remove double negative. I think it's a bit easier to read this way but not needed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, updated

if (!isInCheckNoChangesPass) {
addAfterRenderSequencesForView(lView);
// Sets active consumer to null, ensuring that an improper reactive context is not inherited
const prevConsumer = setActiveConsumer(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't comment on unchanged lines, but in refreshView, we should also update the comment that explains when getActiveConsumer() === null. There are now more situations where the consumer is null than the viewContainerRef.createEmbeddedView(...).detectChanges() edge case. That's absolutely correct -- we just need to update the comment.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. Just like to add that getActiveConsumer() === null would already be the case if you take my original bug report and add OnPush change detection to the ParentComponent

@@ -838,6 +840,51 @@ describe('Angular with scheduler and ZoneJS', () => {
expect(fixture.nativeElement.innerText).toContain('new');
});

it('updating signal inside an EmbeddedView in a child component with OnPush inside a parent component with Default CD', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test would be better in core/test/acceptance/change_detection_spec.ts, maybe under the describe('embedded views', ... or describe('OnPush', ...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the test. Also updated the tests in describe('embedded views', ... to use standalone syntax while I was at it

Copy link
Author

@kristilw kristilw May 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not entirely satisfied with the test name. It feels a bit long, and it does not even convey that the child component has to be only marked for traversal and not dirty. Not sure if best practice in this repo is to have a long test name or add a comment to the test. I'll take your suggestion If you have any.

Copy link
Contributor

@atscott atscott May 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about "does not inherit reactive consumer from ancestor when attached to non-dirty parent"?

Edit: That's pretty implementation-specific but I don't know what else to put. Maybe something along those lines but slightly different...

kristilw added 3 commits May 28, 2025 00:18
update tests to use standalone components for easier test setup
when an embedded view is inside a component with OnPush who has a parent with Default CD, then the embedded view is not updated when a signal in its template is changed

relates to angular#61662
when marked for traversal the reactive context has to be set to null to avoid inheriting the reactive context of the parent component

PR Closes angular#61662
@kristilw kristilw force-pushed the bug-change-detection branch from 5283e46 to 89de2d7 Compare May 27, 2025 22:26
@angular-robot angular-robot bot requested a review from atscott May 27, 2025 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: core Issues related to the framework runtime
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Template with signal not updating when the value of the signal changes
2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy