Skip to content

Commit

Permalink
feat(core): Run afterRender callbacks outside of the Angular zone
Browse files Browse the repository at this point in the history
afterRender should run outside of the Angular zone so that it does not trigger further CD cycles
  • Loading branch information
devknoll committed Aug 16, 2023
1 parent 6145cc1 commit a50345d
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 4 deletions.
10 changes: 7 additions & 3 deletions packages/core/src/render3/after_render_hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {assertInInjectionContext, Injector, ɵɵdefineInjectable} from '../di';
import {inject} from '../di/injector_compatibility';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {DestroyRef} from '../linker/destroy_ref';
import {NgZone} from '../zone';

import {isPlatformBrowser} from './util/misc_utils';

Expand Down Expand Up @@ -188,6 +189,7 @@ class AfterRenderCallback {
* Implements `afterRender` and `afterNextRender` callback manager logic.
*/
export class AfterRenderEventManager {
private zone = inject(NgZone);
private callbacks = new Set<AfterRenderCallback>();
private deferredCallbacks = new Set<AfterRenderCallback>();
private renderDepth = 0;
Expand Down Expand Up @@ -219,9 +221,11 @@ export class AfterRenderEventManager {
if (this.renderDepth === 0) {
try {
this.runningCallbacks = true;
for (const callback of this.callbacks) {
callback.invoke();
}
this.zone.runOutsideAngular(() => {
for (const callback of this.callbacks) {
callback.invoke();
}
});
} finally {
this.runningCallbacks = false;
for (const callback of this.deferredCallbacks) {
Expand Down
48 changes: 47 additions & 1 deletion packages/core/test/acceptance/after_render_hook_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {PLATFORM_BROWSER_ID, PLATFORM_SERVER_ID} from '@angular/common/src/platform_id';
import {afterNextRender, afterRender, AfterRenderRef, ChangeDetectorRef, Component, inject, Injector, PLATFORM_ID, ViewContainerRef} from '@angular/core';
import {afterNextRender, afterRender, AfterRenderRef, ChangeDetectorRef, Component, inject, Injector, NgZone, PLATFORM_ID, ViewContainerRef} from '@angular/core';
import {TestBed} from '@angular/core/testing';

describe('after render hooks', () => {
Expand Down Expand Up @@ -225,6 +225,29 @@ describe('after render hooks', () => {
expect(outerHookCount).toBe(3);
expect(innerHookCount).toBe(2);
});

it('should run outside of the Angular zone', () => {
const zoneLog: boolean[] = [];

@Component({selector: 'comp'})
class Comp {
constructor() {
afterRender(() => {
zoneLog.push(NgZone.isInAngularZone());
});
}
}

TestBed.configureTestingModule({
declarations: [Comp],
...COMMON_CONFIGURATION,
});
const fixture = TestBed.createComponent(Comp);

expect(zoneLog).toEqual([]);
fixture.detectChanges();
expect(zoneLog).toEqual([false]);
});
});

describe('afterNextRender', () => {
Expand Down Expand Up @@ -431,6 +454,29 @@ describe('after render hooks', () => {
expect(outerHookCount).toBe(1);
expect(innerHookCount).toBe(1);
});

it('should run outside of the Angular zone', () => {
const zoneLog: boolean[] = [];

@Component({selector: 'comp'})
class Comp {
constructor() {
afterNextRender(() => {
zoneLog.push(NgZone.isInAngularZone());
});
}
}

TestBed.configureTestingModule({
declarations: [Comp],
...COMMON_CONFIGURATION,
});
const fixture = TestBed.createComponent(Comp);

expect(zoneLog).toEqual([]);
fixture.detectChanges();
expect(zoneLog).toEqual([false]);
});
});
});

Expand Down

0 comments on commit a50345d

Please sign in to comment.