Skip to content

Commit dfcf0d5

Browse files
committed
fix(core): afterRender hooks now only run on ApplicationRef.tick (angular#52455)
The `afterRender` hooks currently run after `ApplicationRef.tick` but also run after any call to `ChangeDetectorRef.detectChanges`. This is problematic because code which uses `afterRender` cannot expect the component it's registered from to be rendered when the callback executes. If there is a call to `ChangeDetectorRef.detectChanges` before the global change detection, that will cause the hooks to run earlier than expected. This behavior is somewhat of a blocker for the zoneless project. There is plenty of application code that do things like `setTimeout(() => doSomethingThatExpectsComponentToBeRendered())`, `NgZone.onStable(() => ...)` or `ApplicationRef.onStable...`. `ApplicationRef.onStable` is a should likely work similarly, but all of these are really wanting an API that is `afterRender` with the requirement that the hook runs after the global render, not an individual CDRef instance. This change updates the `afterRender` hooks to only run when `ApplicationRef.tick` happens. fixes angular#52429 fixes angular#53232 PR Close angular#52455
1 parent a5a9b40 commit dfcf0d5

File tree

10 files changed

+233
-218
lines changed

10 files changed

+233
-218
lines changed

goldens/public-api/core/errors.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,6 @@ export const enum RuntimeErrorCode {
117117
// (undocumented)
118118
RECURSIVE_APPLICATION_REF_TICK = 101,
119119
// (undocumented)
120-
RECURSIVE_APPLICATION_RENDER = 102,
121-
// (undocumented)
122120
RENDERER_NOT_FOUND = 407,
123121
// (undocumented)
124122
REQUIRE_SYNC_WITHOUT_SYNC_EMIT = 601,

packages/core/src/application/application_ref.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {NgModuleFactory, NgModuleRef} from '../linker/ng_module_factory';
2929
import {ViewRef} from '../linker/view_ref';
3030
import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from '../metadata/resource_loading';
3131
import {PendingTasks} from '../pending_tasks';
32+
import {AfterRenderEventManager} from '../render3/after_render_hooks';
3233
import {assertNgModuleType} from '../render3/assert';
3334
import {ComponentFactory as R3ComponentFactory} from '../render3/component_ref';
3435
import {isStandalone} from '../render3/definition';
@@ -323,6 +324,7 @@ export class ApplicationRef {
323324
/** @internal */
324325
_views: InternalViewRef<unknown>[] = [];
325326
private readonly internalErrorHandler = inject(INTERNAL_APPLICATION_ERROR_HANDLER);
327+
private readonly afterRenderEffectManager = inject(AfterRenderEventManager);
326328

327329
/**
328330
* Indicates whether this instance was destroyed.
@@ -555,6 +557,18 @@ export class ApplicationRef {
555557
// Attention: Don't rethrow as it could cancel subscriptions to Observables!
556558
this.internalErrorHandler(e);
557559
} finally {
560+
// Catch any `ExpressionChanged...` errors and report them to error handler like above
561+
try {
562+
const callbacksExecuted = this.afterRenderEffectManager.execute();
563+
if ((typeof ngDevMode === 'undefined' || ngDevMode) && callbacksExecuted) {
564+
for (let view of this._views) {
565+
view.checkNoChanges();
566+
}
567+
}
568+
} catch (e) {
569+
this.internalErrorHandler(e);
570+
}
571+
558572
this._runningTick = false;
559573
}
560574
}

packages/core/src/defer/dom_triggers.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,6 @@ export function onInteraction(trigger: Element, callback: VoidFunction): VoidFun
8383
entry = new DeferEventEntry();
8484
interactionTriggers.set(trigger, entry);
8585

86-
// Ensure that the handler runs in the NgZone
87-
ngDevMode && NgZone.assertInAngularZone();
88-
8986
for (const name of interactionEventNames) {
9087
trigger.addEventListener(name, entry!.listener, eventListenerOptions);
9188
}
@@ -120,9 +117,6 @@ export function onHover(trigger: Element, callback: VoidFunction): VoidFunction
120117
entry = new DeferEventEntry();
121118
hoverTriggers.set(trigger, entry);
122119

123-
// Ensure that the handler runs in the NgZone
124-
ngDevMode && NgZone.assertInAngularZone();
125-
126120
for (const name of hoverEventNames) {
127121
trigger.addEventListener(name, entry!.listener, eventListenerOptions);
128122
}

packages/core/src/errors.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export const enum RuntimeErrorCode {
3030
// Change Detection Errors
3131
EXPRESSION_CHANGED_AFTER_CHECKED = -100,
3232
RECURSIVE_APPLICATION_REF_TICK = 101,
33-
RECURSIVE_APPLICATION_RENDER = 102,
3433
INFINITE_CHANGE_DETECTION = 103,
3534

3635
// Dependency Injection Errors

packages/core/src/render3/after_render_hooks.ts

Lines changed: 17 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -349,13 +349,6 @@ class AfterRenderCallback {
349349
* Implements `afterRender` and `afterNextRender` callback handler logic.
350350
*/
351351
interface AfterRenderCallbackHandler {
352-
/**
353-
* Validate that it's safe for a render operation to begin,
354-
* throwing if not. Not guaranteed to be called if a render
355-
* operation is started before handler was registered.
356-
*/
357-
validateBegin(): void;
358-
359352
/**
360353
* Register a new callback.
361354
*/
@@ -367,9 +360,9 @@ interface AfterRenderCallbackHandler {
367360
unregister(callback: AfterRenderCallback): void;
368361

369362
/**
370-
* Execute callbacks.
363+
* Execute callbacks. Returns `true` if any callbacks were executed.
371364
*/
372-
execute(): void;
365+
execute(): boolean;
373366

374367
/**
375368
* Perform any necessary cleanup.
@@ -392,16 +385,6 @@ class AfterRenderCallbackHandlerImpl implements AfterRenderCallbackHandler {
392385
};
393386
private deferredCallbacks = new Set<AfterRenderCallback>();
394387

395-
validateBegin(): void {
396-
if (this.executingCallbacks) {
397-
throw new RuntimeError(
398-
RuntimeErrorCode.RECURSIVE_APPLICATION_RENDER,
399-
ngDevMode &&
400-
'A new render operation began before the previous operation ended. ' +
401-
'Did you trigger change detection from afterRender or afterNextRender?');
402-
}
403-
}
404-
405388
register(callback: AfterRenderCallback): void {
406389
// If we're currently running callbacks, new callbacks should be deferred
407390
// until the next render operation.
@@ -414,10 +397,12 @@ class AfterRenderCallbackHandlerImpl implements AfterRenderCallbackHandler {
414397
this.deferredCallbacks.delete(callback);
415398
}
416399

417-
execute(): void {
400+
execute(): boolean {
401+
let callbacksExecuted = false;
418402
this.executingCallbacks = true;
419403
for (const bucket of Object.values(this.buckets)) {
420404
for (const callback of bucket) {
405+
callbacksExecuted = true;
421406
callback.invoke();
422407
}
423408
}
@@ -427,6 +412,7 @@ class AfterRenderCallbackHandlerImpl implements AfterRenderCallbackHandler {
427412
this.buckets[callback.phase].add(callback);
428413
}
429414
this.deferredCallbacks.clear();
415+
return callbacksExecuted;
430416
}
431417

432418
destroy(): void {
@@ -442,41 +428,26 @@ class AfterRenderCallbackHandlerImpl implements AfterRenderCallbackHandler {
442428
* Delegates to an optional `AfterRenderCallbackHandler` for implementation.
443429
*/
444430
export class AfterRenderEventManager {
445-
private renderDepth = 0;
446-
447431
/* @internal */
448432
handler: AfterRenderCallbackHandler|null = null;
449433

450434
/* @internal */
451435
internalCallbacks: VoidFunction[] = [];
452436

453437
/**
454-
* Mark the beginning of a render operation (i.e. CD cycle).
455-
* Throws if called while executing callbacks.
438+
* Executes callbacks. Returns `true` if any callbacks executed.
456439
*/
457-
begin() {
458-
this.handler?.validateBegin();
459-
this.renderDepth++;
460-
}
461-
462-
/**
463-
* Mark the end of a render operation. Callbacks will be
464-
* executed if there are no more pending operations.
465-
*/
466-
end() {
467-
ngDevMode && assertGreaterThan(this.renderDepth, 0, 'renderDepth must be greater than 0');
468-
this.renderDepth--;
469-
470-
if (this.renderDepth === 0) {
471-
// Note: internal callbacks power `internalAfterNextRender`. Since internal callbacks
472-
// are fairly trivial, they are kept separate so that `AfterRenderCallbackHandlerImpl`
473-
// can still be tree-shaken unless used by the application.
474-
for (const callback of this.internalCallbacks) {
475-
callback();
476-
}
477-
this.internalCallbacks.length = 0;
478-
this.handler?.execute();
440+
execute(): boolean {
441+
// Note: internal callbacks power `internalAfterNextRender`. Since internal callbacks
442+
// are fairly trivial, they are kept separate so that `AfterRenderCallbackHandlerImpl`
443+
// can still be tree-shaken unless used by the application.
444+
const callbacks = [...this.internalCallbacks];
445+
this.internalCallbacks.length = 0;
446+
for (const callback of callbacks) {
447+
callback();
479448
}
449+
const handlerCallbacksExecuted = this.handler?.execute();
450+
return !!handlerCallbacksExecuted || callbacks.length > 0;
480451
}
481452

482453
ngOnDestroy() {

packages/core/src/render3/instructions/change_detection.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ const MAXIMUM_REFRESH_RERUNS = 100;
3030
export function detectChangesInternal(lView: LView, notifyErrorHandler = true) {
3131
const environment = lView[ENVIRONMENT];
3232
const rendererFactory = environment.rendererFactory;
33-
const afterRenderEventManager = environment.afterRenderEventManager;
3433

3534
// Check no changes mode is a dev only mode used to verify that bindings have not changed
3635
// since they were assigned. We do not want to invoke renderer factory functions in that mode
@@ -39,7 +38,6 @@ export function detectChangesInternal(lView: LView, notifyErrorHandler = true) {
3938

4039
if (!checkNoChangesMode) {
4140
rendererFactory.begin?.();
42-
afterRenderEventManager?.begin();
4341
}
4442

4543
try {
@@ -56,9 +54,6 @@ export function detectChangesInternal(lView: LView, notifyErrorHandler = true) {
5654
// One final flush of the effects queue to catch any effects created in `ngAfterViewInit` or
5755
// other post-order hooks.
5856
environment.inlineEffectRunner?.flush();
59-
60-
// Invoke all callbacks registered via `after*Render`, if needed.
61-
afterRenderEventManager?.end();
6257
}
6358
}
6459
}

0 commit comments

Comments
 (0)