Skip to content

Commit 35326c7

Browse files
committed
fix(component): use setTimeout instead of requestAnimationFrame for server and zoneless
requestAnimationFrame is not available in server-side contexts and will cause errors. Using setTimeout ensures compatibility across all environments.
1 parent 7108d84 commit 35326c7

File tree

2 files changed

+83
-16
lines changed

2 files changed

+83
-16
lines changed

modules/component/spec/core/tick-scheduler.spec.ts

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,24 @@ import {
44
TestBed,
55
tick,
66
} from '@angular/core/testing';
7-
import { ApplicationRef, NgZone } from '@angular/core';
7+
import { ApplicationRef, NgZone, PLATFORM_ID } from '@angular/core';
88
import {
9-
AnimationFrameTickScheduler,
9+
ZonelessTickScheduler,
1010
NoopTickScheduler,
1111
TickScheduler,
1212
} from '../../src/core/tick-scheduler';
1313
import { ngZoneMock, noopNgZoneMock } from '../fixtures/fixtures';
1414

1515
describe('TickScheduler', () => {
16-
function setup(ngZone: unknown) {
16+
function setup(ngZone: unknown, server = false) {
1717
TestBed.configureTestingModule({
18-
providers: [{ provide: NgZone, useValue: ngZone }],
18+
providers: [
19+
{ provide: NgZone, useValue: ngZone },
20+
{
21+
provide: PLATFORM_ID,
22+
useValue: server ? 'server' : 'browser',
23+
},
24+
],
1925
});
2026
const tickScheduler = TestBed.inject(TickScheduler);
2127
const appRef = TestBed.inject(ApplicationRef);
@@ -31,16 +37,16 @@ describe('TickScheduler', () => {
3137
});
3238
});
3339

34-
describe('when NgZone is not provided', () => {
40+
describe('when NgZone is not provided and running in server context', () => {
3541
// `fakeAsync` uses 16ms as `requestAnimationFrame` delay
3642
const animationFrameDelay = 16;
3743

38-
it('should initialize AnimationFrameTickScheduler', () => {
44+
it('should initialize ZonelessTickScheduler', () => {
3945
const { tickScheduler } = setup(noopNgZoneMock);
40-
expect(tickScheduler instanceof AnimationFrameTickScheduler).toBe(true);
46+
expect(tickScheduler instanceof ZonelessTickScheduler).toBe(true);
4147
});
4248

43-
it('should schedule tick using the animationFrameScheduler', fakeAsync(() => {
49+
it('should schedule tick using the ZonelessTickScheduler', fakeAsync(() => {
4450
const { tickScheduler, appRef } = setup(noopNgZoneMock);
4551

4652
tickScheduler.schedule();
@@ -87,4 +93,56 @@ describe('TickScheduler', () => {
8793
expect(appRef.tick).toHaveBeenCalledTimes(3);
8894
}));
8995
});
96+
97+
describe('when NgZone is not provided and running in ssr', () => {
98+
it('should initialize ZonelessTickScheduler', () => {
99+
const { tickScheduler } = setup(noopNgZoneMock, true);
100+
expect(tickScheduler instanceof ZonelessTickScheduler).toBe(true);
101+
});
102+
103+
it('should schedule tick using the ZonelessTickScheduler', fakeAsync(() => {
104+
const { tickScheduler, appRef } = setup(noopNgZoneMock, true);
105+
106+
tickScheduler.schedule();
107+
108+
expect(appRef.tick).toHaveBeenCalledTimes(0);
109+
tick();
110+
expect(appRef.tick).toHaveBeenCalledTimes(1);
111+
}));
112+
113+
it('should coalesce multiple synchronous schedule calls', fakeAsync(() => {
114+
const { tickScheduler, appRef } = setup(noopNgZoneMock, true);
115+
116+
tickScheduler.schedule();
117+
tickScheduler.schedule();
118+
tickScheduler.schedule();
119+
120+
tick();
121+
expect(appRef.tick).toHaveBeenCalledTimes(1);
122+
}));
123+
124+
it('should coalesce multiple schedule calls that are queued to the microtask queue', fakeAsync(() => {
125+
const { tickScheduler, appRef } = setup(noopNgZoneMock, true);
126+
127+
queueMicrotask(() => tickScheduler.schedule());
128+
queueMicrotask(() => tickScheduler.schedule());
129+
queueMicrotask(() => tickScheduler.schedule());
130+
131+
flushMicrotasks();
132+
expect(appRef.tick).toHaveBeenCalledTimes(0);
133+
tick();
134+
expect(appRef.tick).toHaveBeenCalledTimes(1);
135+
}));
136+
137+
it('should schedule multiple ticks for multiple asynchronous schedule calls', fakeAsync(() => {
138+
const { tickScheduler, appRef } = setup(noopNgZoneMock, true);
139+
140+
setTimeout(() => tickScheduler.schedule(), 100);
141+
setTimeout(() => tickScheduler.schedule(), 200);
142+
setTimeout(() => tickScheduler.schedule(), 300);
143+
144+
tick(300);
145+
expect(appRef.tick).toHaveBeenCalledTimes(3);
146+
}));
147+
});
90148
});

modules/component/src/core/tick-scheduler.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
import { ApplicationRef, inject, Injectable, NgZone } from '@angular/core';
1+
import {
2+
ApplicationRef,
3+
inject,
4+
Injectable,
5+
NgZone,
6+
PLATFORM_ID,
7+
} from '@angular/core';
28
import { isNgZone } from './zone-helpers';
9+
import { isPlatformServer } from '@angular/common';
310

411
@Injectable({
512
providedIn: 'root',
613
useFactory: () => {
714
const zone = inject(NgZone);
815
return isNgZone(zone)
916
? new NoopTickScheduler()
10-
: inject(AnimationFrameTickScheduler);
17+
: inject(ZonelessTickScheduler);
1118
},
1219
})
1320
export abstract class TickScheduler {
@@ -17,17 +24,19 @@ export abstract class TickScheduler {
1724
@Injectable({
1825
providedIn: 'root',
1926
})
20-
export class AnimationFrameTickScheduler extends TickScheduler {
27+
export class ZonelessTickScheduler extends TickScheduler {
2128
private isScheduled = false;
22-
23-
constructor(private readonly appRef: ApplicationRef) {
24-
super();
25-
}
29+
private readonly platformId = inject(PLATFORM_ID);
30+
private readonly isServer = isPlatformServer(this.platformId);
31+
private readonly appRef = inject(ApplicationRef);
32+
private readonly scheduleFn = this.isServer
33+
? setTimeout
34+
: requestAnimationFrame;
2635

2736
schedule(): void {
2837
if (!this.isScheduled) {
2938
this.isScheduled = true;
30-
requestAnimationFrame(() => {
39+
this.scheduleFn(() => {
3140
this.appRef.tick();
3241
this.isScheduled = false;
3342
});

0 commit comments

Comments
 (0)