Skip to content

Commit

Permalink
fix(core): Update ApplicationRef.tick loop to only throw in dev mode (#…
Browse files Browse the repository at this point in the history
…54848)

This commit updates the loop in ApplicationRef.tick to only throw in dev
mode. In addition, it reduces the reruns to 10 from 100 in order to
reduce the impact on production applications that encounter this error.
That is, the loop will bail out much earlier and prevent prolonged
unresponsiveness.

PR Close #54848
  • Loading branch information
atscott committed Mar 13, 2024
1 parent 1b19d2d commit 700c052
Show file tree
Hide file tree
Showing 2 changed files with 16 additions and 12 deletions.
26 changes: 15 additions & 11 deletions packages/core/src/application/application_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {PendingTasks} from '../pending_tasks';
import {AfterRenderEventManager} from '../render3/after_render_hooks';
import {ComponentFactory as R3ComponentFactory} from '../render3/component_ref';
import {isStandalone} from '../render3/definition';
import {ChangeDetectionMode, detectChangesInternal, MAXIMUM_REFRESH_RERUNS} from '../render3/instructions/change_detection';
import {ChangeDetectionMode, detectChangesInternal} from '../render3/instructions/change_detection';
import {FLAGS, LView, LViewFlags} from '../render3/interfaces/view';
import {publishDefaultGlobalUtils as _publishDefaultGlobalUtils} from '../render3/util/global_utils';
import {requiresRefreshOrTraversal} from '../render3/util/view_utils';
Expand Down Expand Up @@ -144,6 +144,9 @@ export interface BootstrapOptions {
ngZoneRunCoalescing?: boolean;
}

/** Maximum number of times ApplicationRef will refresh all attached views in a single tick. */
const MAXIMUM_REFRESH_RERUNS = 10;

export function _callAndReportToErrorHandler(
errorHandler: ErrorHandler, ngZone: NgZone, callback: () => any): any {
try {
Expand Down Expand Up @@ -527,15 +530,7 @@ export class ApplicationRef {
private detectChangesInAttachedViews(refreshViews: boolean) {
let runs = 0;
const afterRenderEffectManager = this.afterRenderEffectManager;
while (true) {
if (runs === MAXIMUM_REFRESH_RERUNS) {
throw new RuntimeError(
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
ngDevMode &&
'Infinite change detection while refreshing application views. ' +
'Ensure afterRender or queueStateUpdate hooks are not continuously causing updates.');
}

while (runs < MAXIMUM_REFRESH_RERUNS) {
if (refreshViews) {
const isFirstPass = runs === 0;
this.beforeRender.next(isFirstPass);
Expand All @@ -561,8 +556,17 @@ export class ApplicationRef {
break;
}
}
}

if ((typeof ngDevMode === 'undefined' || ngDevMode) && runs >= MAXIMUM_REFRESH_RERUNS) {
throw new RuntimeError(
RuntimeErrorCode.INFINITE_CHANGE_DETECTION,
ngDevMode &&
'Infinite change detection while refreshing application views. ' +
'Ensure views are not calling `markForCheck` on every template execution or ' +
'that afterRender hooks always mark views for check.',
);
}
}

/**
* Attaches a view so that it will be dirty checked.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/acceptance/after_render_hook_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,7 @@ describe('after render hooks', () => {
appRef.attachView(fixture.componentRef.hostView);
expect(() => {
appRef.tick();
}).toThrowError(/NG0103.*(afterRender|afterNextRender)/);
}).toThrowError(/NG0103.*(Infinite change detection while refreshing application views)/);
});
});

Expand Down

0 comments on commit 700c052

Please sign in to comment.