-
Notifications
You must be signed in to change notification settings - Fork 26.3k
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
Conversation
0662b7f
to
cbd8381
Compare
cbd8381
to
5283e46
Compare
There was a problem hiding this 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:
angular/packages/core/src/render3/instructions/change_detection.ts
Lines 299 to 302 in c11527f
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 () => { |
There was a problem hiding this comment.
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', ...
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it was my repo I would rather have a longer test name. But I'm not deeply familiar with this repo, and I'm mostly concerned with fixing the bug, so I'll go with whatever makes you happy 😁 But I'll try to think of something
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't really matter. Long name is fine. I'm happy if you're happy.
update tests to use standalone components for easier test setup
5283e46
to
89de2d7
Compare
It says that the test job fails. I tried to rerun the one that failed locally but it runs without error. Is it just flaky? |
Yes, the failed test was a flaky one. |
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
89de2d7
to
c3731e0
Compare
Presubmit is "green". |
This PR was merged into the repository by commit 3aa933a. The changes were merged into the following branches: main, 20.0.x |
update tests to use standalone components for easier test setup PR Close #61663
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?
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?
Other information