Skip to content

Commit d5edfde

Browse files
atscottAndrewKushnir
authored andcommitted
fix(core): afterRender hooks registered outside change detection can mark views dirty (#55623)
This commit fixes an error in the looping logic of `ApplicationRef.tick` when the tick skips straight to render hooks. In this case, if a render hook makes a state update that requires a view refresh, we would never actually refresh the view and just loop until we hit the loop limit. PR Close #55623
1 parent 36130b2 commit d5edfde

File tree

2 files changed

+49
-4
lines changed

2 files changed

+49
-4
lines changed

packages/core/src/application/application_ref.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -576,8 +576,10 @@ export class ApplicationRef {
576576
let runs = 0;
577577
const afterRenderEffectManager = this.afterRenderEffectManager;
578578
while (runs < MAXIMUM_REFRESH_RERUNS) {
579-
if (refreshViews) {
580-
const isFirstPass = runs === 0;
579+
const isFirstPass = runs === 0;
580+
// Some notifications to run a `tick` will only trigger render hooks. so we skip refreshing views the first time through.
581+
// After the we execute render hooks in the first pass, we loop while views are marked dirty and should refresh them.
582+
if (refreshViews || !isFirstPass) {
581583
this.beforeRender.next(isFirstPass);
582584
for (let {_lView, notifyErrorHandler} of this._views) {
583585
detectChangesInViewIfRequired(

packages/core/test/acceptance/after_render_hook_spec.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import {withBody} from '@angular/private/testing';
3737

3838
import {destroyPlatform} from '../../src/core';
3939
import {EnvironmentInjector} from '../../src/di';
40+
import {firstValueFrom} from 'rxjs';
41+
import {filter} from 'rxjs/operators';
4042

4143
function createAndAttachComponent<T>(component: Type<T>) {
4244
const componentRef = createComponent(component, {
@@ -1097,7 +1099,7 @@ describe('after render hooks', () => {
10971099
expect(fixture.nativeElement.innerText).toBe('1');
10981100
});
10991101

1100-
it('allows updating state and calling markForCheck in afterRender', () => {
1102+
it('allows updating state and calling markForCheck in afterRender', async () => {
11011103
@Component({
11021104
selector: 'test-component',
11031105
standalone: true,
@@ -1124,7 +1126,48 @@ describe('after render hooks', () => {
11241126
const fixture = TestBed.createComponent(TestCmp);
11251127
const appRef = TestBed.inject(ApplicationRef);
11261128
appRef.attachView(fixture.componentRef.hostView);
1127-
appRef.tick();
1129+
await firstValueFrom(appRef.isStable.pipe(filter((stable) => stable)));
1130+
expect(fixture.nativeElement.innerText).toBe('1');
1131+
});
1132+
1133+
it('allows updating state and calling markForCheck in afterRender, outside of change detection', async () => {
1134+
const counter = signal(0);
1135+
@Component({
1136+
selector: 'test-component',
1137+
standalone: true,
1138+
template: `{{counter()}}`,
1139+
})
1140+
class TestCmp {
1141+
injector = inject(EnvironmentInjector);
1142+
counter = counter;
1143+
async ngOnInit() {
1144+
// push the render hook to a time outside of change detection
1145+
await new Promise<void>((resolve) => setTimeout(resolve));
1146+
afterNextRender(
1147+
() => {
1148+
counter.set(1);
1149+
},
1150+
{injector: this.injector},
1151+
);
1152+
}
1153+
}
1154+
TestBed.configureTestingModule({
1155+
providers: [{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID}],
1156+
});
1157+
1158+
const fixture = TestBed.createComponent(TestCmp);
1159+
const appRef = TestBed.inject(ApplicationRef);
1160+
appRef.attachView(fixture.componentRef.hostView);
1161+
await new Promise<void>((resolve) => {
1162+
TestBed.runInInjectionContext(() => {
1163+
effect(() => {
1164+
if (counter() === 1) {
1165+
resolve();
1166+
}
1167+
});
1168+
});
1169+
});
1170+
await firstValueFrom(appRef.isStable.pipe(filter((stable) => stable)));
11281171
expect(fixture.nativeElement.innerText).toBe('1');
11291172
});
11301173

0 commit comments

Comments
 (0)