Split recent passive effects changes into 2 flags (#18030)
* Split recent passive effects changes into 2 flags Separate flags can now be used to opt passive effects into: 1) Deferring destroy functions on unmount to subsequent passive effects flush 2) Running all destroy functions (for all fibers) before create functions This allows us to test the less risky feature (2) separately from the more risky one. * deferPassiveEffectCleanupDuringUnmount is ignored unless runAllPassiveEffectDestroysBeforeCreates is true
Brian Vaughn committed
Feb 18, 2020 at 14:19 UTC
691096c95d1019f57e0da2c9a060c5e094b7c586
12 files changed
+5400
-5101
packages/react-reconciler/src/ReactFiberCommitWork.js
+7
-3
@@ -34,6 +34,7 @@ import {
34
enableFundamentalAPI,
35
enableSuspenseCallback,
36
enableScopeAPI,
37
+ runAllPassiveEffectDestroysBeforeCreates,
38
} from 'shared/ReactFeatureFlags';
39
import {
40
FunctionComponent,
@@ -398,7 +399,7 @@ function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
399
}
400
401
function schedulePassiveEffects(finishedWork: Fiber) {
401
- if (deferPassiveEffectCleanupDuringUnmount) {
402
+ if (runAllPassiveEffectDestroysBeforeCreates) {
403
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
404
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
405
if (lastEffect !== null) {
@@ -456,7 +457,7 @@ function commitLifeCycles(
457
// by a create function in another component during the same commit.
458
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
459
459
- if (deferPassiveEffectCleanupDuringUnmount) {
460
+ if (runAllPassiveEffectDestroysBeforeCreates) {
461
schedulePassiveEffects(finishedWork);
462
}
463
return;
@@ -795,7 +796,10 @@ function commitUnmount(
796
if (lastEffect !== null) {
797
const firstEffect = lastEffect.next;
798
798
- if (deferPassiveEffectCleanupDuringUnmount) {
799
+ if (
800
+ deferPassiveEffectCleanupDuringUnmount &&
801
+ runAllPassiveEffectDestroysBeforeCreates
802
+ ) {
803
let effect = firstEffect;
804
do {
805
const {destroy, tag} = effect;
packages/react-reconciler/src/ReactFiberWorkLoop.js
+4
-4
@@ -18,7 +18,7 @@ import type {Effect as HookEffect} from './ReactFiberHooks';
18
19
import {
20
warnAboutDeprecatedLifecycles,
21
- deferPassiveEffectCleanupDuringUnmount,
21
+ runAllPassiveEffectDestroysBeforeCreates,
22
enableUserTimingAPI,
23
enableSuspenseServerRenderer,
24
replayFailedUnitOfWorkWithInvokeGuardedCallback,
@@ -2174,7 +2174,7 @@ export function enqueuePendingPassiveHookEffectMount(
2174
fiber: Fiber,
2175
effect: HookEffect,
2176
): void {
2177
- if (deferPassiveEffectCleanupDuringUnmount) {
2177
+ if (runAllPassiveEffectDestroysBeforeCreates) {
2178
pendingPassiveHookEffectsMount.push(effect, fiber);
2179
if (!rootDoesHavePassiveEffects) {
2180
rootDoesHavePassiveEffects = true;
@@ -2190,7 +2190,7 @@ export function enqueuePendingPassiveHookEffectUnmount(
2190
fiber: Fiber,
2191
effect: HookEffect,
2192
): void {
2193
- if (deferPassiveEffectCleanupDuringUnmount) {
2193
+ if (runAllPassiveEffectDestroysBeforeCreates) {
2194
pendingPassiveHookEffectsUnmount.push(effect, fiber);
2195
if (!rootDoesHavePassiveEffects) {
2196
rootDoesHavePassiveEffects = true;
@@ -2224,7 +2224,7 @@ function flushPassiveEffectsImpl() {
2224
executionContext |= CommitContext;
2225
const prevInteractions = pushInteractions(root);
2226
2227
- if (deferPassiveEffectCleanupDuringUnmount) {
2227
+ if (runAllPassiveEffectDestroysBeforeCreates) {
2228
// It's important that ALL pending passive effect destroy functions are called
2229
// before ANY passive effect create functions are called.
2230
// Otherwise effects in sibling components might interfere with each other.
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+2663
-2469
@@ -34,2759 +34,2953 @@ let forwardRef;
34
let memo;
35
let act;
36
37
-describe('ReactHooksWithNoopRenderer', () => {
38
- beforeEach(() => {
39
- jest.resetModules();
40
- jest.useFakeTimers();
41
-
42
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
43
- ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
44
- ReactFeatureFlags.enableSchedulerTracing = true;
45
- ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
46
- ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = true;
47
- React = require('react');
48
- ReactNoop = require('react-noop-renderer');
49
- Scheduler = require('scheduler');
50
- SchedulerTracing = require('scheduler/tracing');
51
- ReactCache = require('react-cache');
52
- useState = React.useState;
53
- useReducer = React.useReducer;
54
- useEffect = React.useEffect;
55
- useLayoutEffect = React.useLayoutEffect;
56
- useCallback = React.useCallback;
57
- useMemo = React.useMemo;
58
- useRef = React.useRef;
59
- useImperativeHandle = React.useImperativeHandle;
60
- forwardRef = React.forwardRef;
61
- memo = React.memo;
62
- useTransition = React.useTransition;
63
- useDeferredValue = React.useDeferredValue;
64
- Suspense = React.Suspense;
65
- act = ReactNoop.act;
66
-
67
- TextResource = ReactCache.unstable_createResource(
68
- ([text, ms = 0]) => {
69
- return new Promise((resolve, reject) =>
70
- setTimeout(() => {
71
- Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
72
- resolve(text);
73
- }, ms),
37
+function loadModules({
38
+ deferPassiveEffectCleanupDuringUnmount,
39
+ runAllPassiveEffectDestroysBeforeCreates,
40
+}) {
41
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
42
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
43
+ ReactFeatureFlags.enableSchedulerTracing = true;
44
+ ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
45
+ ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = deferPassiveEffectCleanupDuringUnmount;
46
+ ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates = runAllPassiveEffectDestroysBeforeCreates;
47
+ React = require('react');
48
+ ReactNoop = require('react-noop-renderer');
49
+ Scheduler = require('scheduler');
50
+ SchedulerTracing = require('scheduler/tracing');
51
+ ReactCache = require('react-cache');
52
+ useState = React.useState;
53
+ useReducer = React.useReducer;
54
+ useEffect = React.useEffect;
55
+ useLayoutEffect = React.useLayoutEffect;
56
+ useCallback = React.useCallback;
57
+ useMemo = React.useMemo;
58
+ useRef = React.useRef;
59
+ useImperativeHandle = React.useImperativeHandle;
60
+ forwardRef = React.forwardRef;
61
+ memo = React.memo;
62
+ useTransition = React.useTransition;
63
+ useDeferredValue = React.useDeferredValue;
64
+ Suspense = React.Suspense;
65
+ act = ReactNoop.act;
66
+}
67
+
68
+[true, false].forEach(deferPassiveEffectCleanupDuringUnmount => {
69
+ [true, false].forEach(runAllPassiveEffectDestroysBeforeCreates => {
70
+ describe(`ReactHooksWithNoopRenderer deferPassiveEffectCleanupDuringUnmount:${deferPassiveEffectCleanupDuringUnmount} runAllPassiveEffectDestroysBeforeCreates:${runAllPassiveEffectDestroysBeforeCreates}`, () => {
71
+ beforeEach(() => {
72
+ jest.resetModules();
73
+ jest.useFakeTimers();
74
+
75
+ loadModules({
76
+ deferPassiveEffectCleanupDuringUnmount,
77
+ runAllPassiveEffectDestroysBeforeCreates,
78
+ });
79
+
80
+ TextResource = ReactCache.unstable_createResource(
81
+ ([text, ms = 0]) => {
82
+ return new Promise((resolve, reject) =>
83
+ setTimeout(() => {
84
+ Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
85
+ resolve(text);
86
+ }, ms),
87
+ );
88
+ },
89
+ ([text, ms]) => text,
90
);
75
- },
76
- ([text, ms]) => text,
77
- );
78
- });
91
+ });
92
80
- function span(prop) {
81
- return {type: 'span', hidden: false, children: [], prop};
82
- }
83
-
84
- function hiddenSpan(prop) {
85
- return {type: 'span', children: [], prop, hidden: true};
86
- }
87
-
88
- function Text(props) {
89
- Scheduler.unstable_yieldValue(props.text);
90
- return <span prop={props.text} />;
91
- }
92
-
93
- function AsyncText(props) {
94
- const text = props.text;
95
- try {
96
- TextResource.read([props.text, props.ms]);
97
- Scheduler.unstable_yieldValue(text);
98
- return <span prop={text} />;
99
- } catch (promise) {
100
- if (typeof promise.then === 'function') {
101
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
102
- } else {
103
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
93
+ function span(prop) {
94
+ return {type: 'span', hidden: false, children: [], prop};
95
}
105
- throw promise;
106
- }
107
- }
108
-
109
- function advanceTimers(ms) {
110
- // Note: This advances Jest's virtual time but not React's. Use
111
- // ReactNoop.expire for that.
112
- if (typeof ms !== 'number') {
113
- throw new Error('Must specify ms');
114
- }
115
- jest.advanceTimersByTime(ms);
116
- // Wait until the end of the current tick
117
- // We cannot use a timer since we're faking them
118
- return Promise.resolve().then(() => {});
119
- }
120
-
121
- it('resumes after an interruption', () => {
122
- function Counter(props, ref) {
123
- const [count, updateCount] = useState(0);
124
- useImperativeHandle(ref, () => ({updateCount}));
125
- return <Text text={props.label + ': ' + count} />;
126
- }
127
- Counter = forwardRef(Counter);
128
-
129
- // Initial mount
130
- const counter = React.createRef(null);
131
- ReactNoop.render(<Counter label="Count" ref={counter} />);
132
- expect(Scheduler).toFlushAndYield(['Count: 0']);
133
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
134
-
135
- // Schedule some updates
136
- ReactNoop.batchedUpdates(() => {
137
- counter.current.updateCount(1);
138
- counter.current.updateCount(count => count + 10);
139
- });
96
141
- // Partially flush without committing
142
- expect(Scheduler).toFlushAndYieldThrough(['Count: 11']);
143
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
97
+ function hiddenSpan(prop) {
98
+ return {type: 'span', children: [], prop, hidden: true};
99
+ }
100
145
- // Interrupt with a high priority update
146
- ReactNoop.flushSync(() => {
147
- ReactNoop.render(<Counter label="Total" />);
148
- });
149
- expect(Scheduler).toHaveYielded(['Total: 0']);
101
+ function Text(props) {
102
+ Scheduler.unstable_yieldValue(props.text);
103
+ return <span prop={props.text} />;
104
+ }
105
151
- // Resume rendering
152
- expect(Scheduler).toFlushAndYield(['Total: 11']);
153
- expect(ReactNoop.getChildren()).toEqual([span('Total: 11')]);
154
- });
106
+ function AsyncText(props) {
107
+ const text = props.text;
108
+ try {
109
+ TextResource.read([props.text, props.ms]);
110
+ Scheduler.unstable_yieldValue(text);
111
+ return <span prop={text} />;
112
+ } catch (promise) {
113
+ if (typeof promise.then === 'function') {
114
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
115
+ } else {
116
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
117
+ }
118
+ throw promise;
119
+ }
120
+ }
121
156
- it('throws inside class components', () => {
157
- class BadCounter extends React.Component {
158
- render() {
159
- const [count] = useState(0);
160
- return <Text text={this.props.label + ': ' + count} />;
122
+ function advanceTimers(ms) {
123
+ // Note: This advances Jest's virtual time but not React's. Use
124
+ // ReactNoop.expire for that.
125
+ if (typeof ms !== 'number') {
126
+ throw new Error('Must specify ms');
127
+ }
128
+ jest.advanceTimersByTime(ms);
129
+ // Wait until the end of the current tick
130
+ // We cannot use a timer since we're faking them
131
+ return Promise.resolve().then(() => {});
132
}
162
- }
163
- ReactNoop.render(<BadCounter />);
164
-
165
- expect(Scheduler).toFlushAndThrow(
166
- 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
167
- ' one of the following reasons:\n' +
168
- '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
169
- '2. You might be breaking the Rules of Hooks\n' +
170
- '3. You might have more than one copy of React in the same app\n' +
171
- 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
172
- );
173
-
174
- // Confirm that a subsequent hook works properly.
175
- function GoodCounter(props, ref) {
176
- const [count] = useState(props.initialCount);
177
- return <Text text={count} />;
178
- }
179
- ReactNoop.render(<GoodCounter initialCount={10} />);
180
- expect(Scheduler).toFlushAndYield([10]);
181
- });
133
183
- it('throws inside module-style components', () => {
184
- function Counter() {
185
- return {
186
- render() {
187
- const [count] = useState(0);
188
- return <Text text={this.props.label + ': ' + count} />;
189
- },
190
- };
191
- }
192
- ReactNoop.render(<Counter />);
193
- expect(() =>
194
- expect(Scheduler).toFlushAndThrow(
195
- 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen ' +
196
- 'for one of the following reasons:\n' +
197
- '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
198
- '2. You might be breaking the Rules of Hooks\n' +
199
- '3. You might have more than one copy of React in the same app\n' +
200
- 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
201
- ),
202
- ).toErrorDev(
203
- 'Warning: The <Counter /> component appears to be a function component that returns a class instance. ' +
204
- 'Change Counter to a class that extends React.Component instead. ' +
205
- "If you can't use a class try assigning the prototype on the function as a workaround. " +
206
- '`Counter.prototype = React.Component.prototype`. ' +
207
- "Don't use an arrow function since it cannot be called with `new` by React.",
208
- );
209
-
210
- // Confirm that a subsequent hook works properly.
211
- function GoodCounter(props) {
212
- const [count] = useState(props.initialCount);
213
- return <Text text={count} />;
214
- }
215
- ReactNoop.render(<GoodCounter initialCount={10} />);
216
- expect(Scheduler).toFlushAndYield([10]);
217
- });
134
+ it('resumes after an interruption', () => {
135
+ function Counter(props, ref) {
136
+ const [count, updateCount] = useState(0);
137
+ useImperativeHandle(ref, () => ({updateCount}));
138
+ return <Text text={props.label + ': ' + count} />;
139
+ }
140
+ Counter = forwardRef(Counter);
141
219
- it('throws when called outside the render phase', () => {
220
- expect(() => useState(0)).toThrow(
221
- 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
222
- ' one of the following reasons:\n' +
223
- '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
224
- '2. You might be breaking the Rules of Hooks\n' +
225
- '3. You might have more than one copy of React in the same app\n' +
226
- 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
227
- );
228
- });
142
+ // Initial mount
143
+ const counter = React.createRef(null);
144
+ ReactNoop.render(<Counter label="Count" ref={counter} />);
145
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
146
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
147
230
- describe('useState', () => {
231
- it('simple mount and update', () => {
232
- function Counter(props, ref) {
233
- const [count, updateCount] = useState(0);
234
- useImperativeHandle(ref, () => ({updateCount}));
235
- return <Text text={'Count: ' + count} />;
236
- }
237
- Counter = forwardRef(Counter);
238
- const counter = React.createRef(null);
239
- ReactNoop.render(<Counter ref={counter} />);
240
- expect(Scheduler).toFlushAndYield(['Count: 0']);
241
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
242
-
243
- act(() => counter.current.updateCount(1));
244
- expect(Scheduler).toHaveYielded(['Count: 1']);
245
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
246
-
247
- act(() => counter.current.updateCount(count => count + 10));
248
- expect(Scheduler).toHaveYielded(['Count: 11']);
249
- expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
250
- });
148
+ // Schedule some updates
149
+ ReactNoop.batchedUpdates(() => {
150
+ counter.current.updateCount(1);
151
+ counter.current.updateCount(count => count + 10);
152
+ });
153
+
154
+ // Partially flush without committing
155
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 11']);
156
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
157
252
- it('lazy state initializer', () => {
253
- function Counter(props, ref) {
254
- const [count, updateCount] = useState(() => {
255
- Scheduler.unstable_yieldValue('getInitialState');
256
- return props.initialState;
158
+ // Interrupt with a high priority update
159
+ ReactNoop.flushSync(() => {
160
+ ReactNoop.render(<Counter label="Total" />);
161
});
258
- useImperativeHandle(ref, () => ({updateCount}));
259
- return <Text text={'Count: ' + count} />;
260
- }
261
- Counter = forwardRef(Counter);
262
- const counter = React.createRef(null);
263
- ReactNoop.render(<Counter initialState={42} ref={counter} />);
264
- expect(Scheduler).toFlushAndYield(['getInitialState', 'Count: 42']);
265
- expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]);
266
-
267
- act(() => counter.current.updateCount(7));
268
- expect(Scheduler).toHaveYielded(['Count: 7']);
269
- expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]);
270
- });
162
+ expect(Scheduler).toHaveYielded(['Total: 0']);
163
272
- it('multiple states', () => {
273
- function Counter(props, ref) {
274
- const [count, updateCount] = useState(0);
275
- const [label, updateLabel] = useState('Count');
276
- useImperativeHandle(ref, () => ({updateCount, updateLabel}));
277
- return <Text text={label + ': ' + count} />;
278
- }
279
- Counter = forwardRef(Counter);
280
- const counter = React.createRef(null);
281
- ReactNoop.render(<Counter ref={counter} />);
282
- expect(Scheduler).toFlushAndYield(['Count: 0']);
283
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
164
+ // Resume rendering
165
+ expect(Scheduler).toFlushAndYield(['Total: 11']);
166
+ expect(ReactNoop.getChildren()).toEqual([span('Total: 11')]);
167
+ });
168
285
- act(() => counter.current.updateCount(7));
286
- expect(Scheduler).toHaveYielded(['Count: 7']);
169
+ it('throws inside class components', () => {
170
+ class BadCounter extends React.Component {
171
+ render() {
172
+ const [count] = useState(0);
173
+ return <Text text={this.props.label + ': ' + count} />;
174
+ }
175
+ }
176
+ ReactNoop.render(<BadCounter />);
177
+
178
+ expect(Scheduler).toFlushAndThrow(
179
+ 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
180
+ ' one of the following reasons:\n' +
181
+ '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
182
+ '2. You might be breaking the Rules of Hooks\n' +
183
+ '3. You might have more than one copy of React in the same app\n' +
184
+ 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
185
+ );
186
288
- act(() => counter.current.updateLabel('Total'));
289
- expect(Scheduler).toHaveYielded(['Total: 7']);
290
- });
187
+ // Confirm that a subsequent hook works properly.
188
+ function GoodCounter(props, ref) {
189
+ const [count] = useState(props.initialCount);
190
+ return <Text text={count} />;
191
+ }
192
+ ReactNoop.render(<GoodCounter initialCount={10} />);
193
+ expect(Scheduler).toFlushAndYield([10]);
194
+ });
195
292
- it('returns the same updater function every time', () => {
293
- let updaters = [];
294
- function Counter() {
295
- const [count, updateCount] = useState(0);
296
- updaters.push(updateCount);
297
- return <Text text={'Count: ' + count} />;
298
- }
299
- ReactNoop.render(<Counter />);
300
- expect(Scheduler).toFlushAndYield(['Count: 0']);
301
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
196
+ it('throws inside module-style components', () => {
197
+ function Counter() {
198
+ return {
199
+ render() {
200
+ const [count] = useState(0);
201
+ return <Text text={this.props.label + ': ' + count} />;
202
+ },
203
+ };
204
+ }
205
+ ReactNoop.render(<Counter />);
206
+ expect(() =>
207
+ expect(Scheduler).toFlushAndThrow(
208
+ 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen ' +
209
+ 'for one of the following reasons:\n' +
210
+ '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
211
+ '2. You might be breaking the Rules of Hooks\n' +
212
+ '3. You might have more than one copy of React in the same app\n' +
213
+ 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
214
+ ),
215
+ ).toErrorDev(
216
+ 'Warning: The <Counter /> component appears to be a function component that returns a class instance. ' +
217
+ 'Change Counter to a class that extends React.Component instead. ' +
218
+ "If you can't use a class try assigning the prototype on the function as a workaround. " +
219
+ '`Counter.prototype = React.Component.prototype`. ' +
220
+ "Don't use an arrow function since it cannot be called with `new` by React.",
221
+ );
222
303
- act(() => updaters[0](1));
304
- expect(Scheduler).toHaveYielded(['Count: 1']);
305
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
223
+ // Confirm that a subsequent hook works properly.
224
+ function GoodCounter(props) {
225
+ const [count] = useState(props.initialCount);
226
+ return <Text text={count} />;
227
+ }
228
+ ReactNoop.render(<GoodCounter initialCount={10} />);
229
+ expect(Scheduler).toFlushAndYield([10]);
230
+ });
231
307
- act(() => updaters[0](count => count + 10));
308
- expect(Scheduler).toHaveYielded(['Count: 11']);
309
- expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
232
+ it('throws when called outside the render phase', () => {
233
+ expect(() => useState(0)).toThrow(
234
+ 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
235
+ ' one of the following reasons:\n' +
236
+ '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
237
+ '2. You might be breaking the Rules of Hooks\n' +
238
+ '3. You might have more than one copy of React in the same app\n' +
239
+ 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
240
+ );
241
+ });
242
311
- expect(updaters).toEqual([updaters[0], updaters[0], updaters[0]]);
312
- });
243
+ describe('useState', () => {
244
+ it('simple mount and update', () => {
245
+ function Counter(props, ref) {
246
+ const [count, updateCount] = useState(0);
247
+ useImperativeHandle(ref, () => ({updateCount}));
248
+ return <Text text={'Count: ' + count} />;
249
+ }
250
+ Counter = forwardRef(Counter);
251
+ const counter = React.createRef(null);
252
+ ReactNoop.render(<Counter ref={counter} />);
253
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
254
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
255
+
256
+ act(() => counter.current.updateCount(1));
257
+ expect(Scheduler).toHaveYielded(['Count: 1']);
258
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
259
+
260
+ act(() => counter.current.updateCount(count => count + 10));
261
+ expect(Scheduler).toHaveYielded(['Count: 11']);
262
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
263
+ });
264
314
- it('warns on set after unmount', () => {
315
- let _updateCount;
316
- function Counter(props, ref) {
317
- const [, updateCount] = useState(0);
318
- _updateCount = updateCount;
319
- return null;
320
- }
265
+ it('lazy state initializer', () => {
266
+ function Counter(props, ref) {
267
+ const [count, updateCount] = useState(() => {
268
+ Scheduler.unstable_yieldValue('getInitialState');
269
+ return props.initialState;
270
+ });
271
+ useImperativeHandle(ref, () => ({updateCount}));
272
+ return <Text text={'Count: ' + count} />;
273
+ }
274
+ Counter = forwardRef(Counter);
275
+ const counter = React.createRef(null);
276
+ ReactNoop.render(<Counter initialState={42} ref={counter} />);
277
+ expect(Scheduler).toFlushAndYield(['getInitialState', 'Count: 42']);
278
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]);
279
+
280
+ act(() => counter.current.updateCount(7));
281
+ expect(Scheduler).toHaveYielded(['Count: 7']);
282
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]);
283
+ });
284
322
- ReactNoop.render(<Counter />);
323
- expect(Scheduler).toFlushWithoutYielding();
324
- ReactNoop.render(null);
325
- expect(Scheduler).toFlushWithoutYielding();
326
- expect(() => act(() => _updateCount(1))).toErrorDev(
327
- "Warning: Can't perform a React state update on an unmounted " +
328
- 'component. This is a no-op, but it indicates a memory leak in your ' +
329
- 'application. To fix, cancel all subscriptions and asynchronous ' +
330
- 'tasks in a useEffect cleanup function.\n' +
331
- ' in Counter (at **)',
332
- );
333
- });
285
+ it('multiple states', () => {
286
+ function Counter(props, ref) {
287
+ const [count, updateCount] = useState(0);
288
+ const [label, updateLabel] = useState('Count');
289
+ useImperativeHandle(ref, () => ({updateCount, updateLabel}));
290
+ return <Text text={label + ': ' + count} />;
291
+ }
292
+ Counter = forwardRef(Counter);
293
+ const counter = React.createRef(null);
294
+ ReactNoop.render(<Counter ref={counter} />);
295
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
296
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
297
335
- it('works with memo', () => {
336
- let _updateCount;
337
- function Counter(props) {
338
- const [count, updateCount] = useState(0);
339
- _updateCount = updateCount;
340
- return <Text text={'Count: ' + count} />;
341
- }
342
- Counter = memo(Counter);
298
+ act(() => counter.current.updateCount(7));
299
+ expect(Scheduler).toHaveYielded(['Count: 7']);
300
344
- ReactNoop.render(<Counter />);
345
- expect(Scheduler).toFlushAndYield(['Count: 0']);
346
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
301
+ act(() => counter.current.updateLabel('Total'));
302
+ expect(Scheduler).toHaveYielded(['Total: 7']);
303
+ });
304
348
- ReactNoop.render(<Counter />);
349
- expect(Scheduler).toFlushAndYield([]);
350
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
305
+ it('returns the same updater function every time', () => {
306
+ let updaters = [];
307
+ function Counter() {
308
+ const [count, updateCount] = useState(0);
309
+ updaters.push(updateCount);
310
+ return <Text text={'Count: ' + count} />;
311
+ }
312
+ ReactNoop.render(<Counter />);
313
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
314
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
315
352
- act(() => _updateCount(1));
353
- expect(Scheduler).toHaveYielded(['Count: 1']);
354
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
355
- });
356
- });
316
+ act(() => updaters[0](1));
317
+ expect(Scheduler).toHaveYielded(['Count: 1']);
318
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
319
358
- describe('updates during the render phase', () => {
359
- it('restarts the render function and applies the new updates on top', () => {
360
- function ScrollView({row: newRow}) {
361
- let [isScrollingDown, setIsScrollingDown] = useState(false);
362
- let [row, setRow] = useState(null);
320
+ act(() => updaters[0](count => count + 10));
321
+ expect(Scheduler).toHaveYielded(['Count: 11']);
322
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
323
364
- if (row !== newRow) {
365
- // Row changed since last render. Update isScrollingDown.
366
- setIsScrollingDown(row !== null && newRow > row);
367
- setRow(newRow);
368
- }
324
+ expect(updaters).toEqual([updaters[0], updaters[0], updaters[0]]);
325
+ });
326
370
- return <Text text={`Scrolling down: ${isScrollingDown}`} />;
371
- }
327
+ it('warns on set after unmount', () => {
328
+ let _updateCount;
329
+ function Counter(props, ref) {
330
+ const [, updateCount] = useState(0);
331
+ _updateCount = updateCount;
332
+ return null;
333
+ }
334
373
- ReactNoop.render(<ScrollView row={1} />);
374
- expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
375
- expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: false')]);
335
+ ReactNoop.render(<Counter />);
336
+ expect(Scheduler).toFlushWithoutYielding();
337
+ ReactNoop.render(null);
338
+ expect(Scheduler).toFlushWithoutYielding();
339
+ expect(() => act(() => _updateCount(1))).toErrorDev(
340
+ "Warning: Can't perform a React state update on an unmounted " +
341
+ 'component. This is a no-op, but it indicates a memory leak in your ' +
342
+ 'application. To fix, cancel all subscriptions and asynchronous ' +
343
+ 'tasks in a useEffect cleanup function.\n' +
344
+ ' in Counter (at **)',
345
+ );
346
+ });
347
377
- ReactNoop.render(<ScrollView row={5} />);
378
- expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
379
- expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: true')]);
348
+ it('works with memo', () => {
349
+ let _updateCount;
350
+ function Counter(props) {
351
+ const [count, updateCount] = useState(0);
352
+ _updateCount = updateCount;
353
+ return <Text text={'Count: ' + count} />;
354
+ }
355
+ Counter = memo(Counter);
356
381
- ReactNoop.render(<ScrollView row={5} />);
382
- expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
383
- expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: true')]);
357
+ ReactNoop.render(<Counter />);
358
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
359
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
360
385
- ReactNoop.render(<ScrollView row={10} />);
386
- expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
387
- expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: true')]);
361
+ ReactNoop.render(<Counter />);
362
+ expect(Scheduler).toFlushAndYield([]);
363
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
364
389
- ReactNoop.render(<ScrollView row={2} />);
390
- expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
391
- expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: false')]);
365
+ act(() => _updateCount(1));
366
+ expect(Scheduler).toHaveYielded(['Count: 1']);
367
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
368
+ });
369
+ });
370
393
- ReactNoop.render(<ScrollView row={2} />);
394
- expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
395
- expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: false')]);
396
- });
371
+ describe('updates during the render phase', () => {
372
+ it('restarts the render function and applies the new updates on top', () => {
373
+ function ScrollView({row: newRow}) {
374
+ let [isScrollingDown, setIsScrollingDown] = useState(false);
375
+ let [row, setRow] = useState(null);
376
398
- it('keeps restarting until there are no more new updates', () => {
399
- function Counter({row: newRow}) {
400
- let [count, setCount] = useState(0);
401
- if (count < 3) {
402
- setCount(count + 1);
403
- }
404
- Scheduler.unstable_yieldValue('Render: ' + count);
405
- return <Text text={count} />;
406
- }
377
+ if (row !== newRow) {
378
+ // Row changed since last render. Update isScrollingDown.
379
+ setIsScrollingDown(row !== null && newRow > row);
380
+ setRow(newRow);
381
+ }
382
408
- ReactNoop.render(<Counter />);
409
- expect(Scheduler).toFlushAndYield([
410
- 'Render: 0',
411
- 'Render: 1',
412
- 'Render: 2',
413
- 'Render: 3',
414
- 3,
415
- ]);
416
- expect(ReactNoop.getChildren()).toEqual([span(3)]);
417
- });
383
+ return <Text text={`Scrolling down: ${isScrollingDown}`} />;
384
+ }
385
419
- it('updates multiple times within same render function', () => {
420
- function Counter({row: newRow}) {
421
- let [count, setCount] = useState(0);
422
- if (count < 12) {
423
- setCount(c => c + 1);
424
- setCount(c => c + 1);
425
- setCount(c => c + 1);
426
- }
427
- Scheduler.unstable_yieldValue('Render: ' + count);
428
- return <Text text={count} />;
429
- }
386
+ ReactNoop.render(<ScrollView row={1} />);
387
+ expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
388
+ expect(ReactNoop.getChildren()).toEqual([
389
+ span('Scrolling down: false'),
390
+ ]);
391
431
- ReactNoop.render(<Counter />);
432
- expect(Scheduler).toFlushAndYield([
433
- // Should increase by three each time
434
- 'Render: 0',
435
- 'Render: 3',
436
- 'Render: 6',
437
- 'Render: 9',
438
- 'Render: 12',
439
- 12,
440
- ]);
441
- expect(ReactNoop.getChildren()).toEqual([span(12)]);
442
- });
392
+ ReactNoop.render(<ScrollView row={5} />);
393
+ expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
394
+ expect(ReactNoop.getChildren()).toEqual([
395
+ span('Scrolling down: true'),
396
+ ]);
397
444
- it('throws after too many iterations', () => {
445
- function Counter({row: newRow}) {
446
- let [count, setCount] = useState(0);
447
- setCount(count + 1);
448
- Scheduler.unstable_yieldValue('Render: ' + count);
449
- return <Text text={count} />;
450
- }
451
- ReactNoop.render(<Counter />);
452
- expect(Scheduler).toFlushAndThrow(
453
- 'Too many re-renders. React limits the number of renders to prevent ' +
454
- 'an infinite loop.',
455
- );
456
- });
398
+ ReactNoop.render(<ScrollView row={5} />);
399
+ expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
400
+ expect(ReactNoop.getChildren()).toEqual([
401
+ span('Scrolling down: true'),
402
+ ]);
403
458
- it('works with useReducer', () => {
459
- function reducer(state, action) {
460
- return action === 'increment' ? state + 1 : state;
461
- }
462
- function Counter({row: newRow}) {
463
- let [count, dispatch] = useReducer(reducer, 0);
464
- if (count < 3) {
465
- dispatch('increment');
466
- }
467
- Scheduler.unstable_yieldValue('Render: ' + count);
468
- return <Text text={count} />;
469
- }
404
+ ReactNoop.render(<ScrollView row={10} />);
405
+ expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
406
+ expect(ReactNoop.getChildren()).toEqual([
407
+ span('Scrolling down: true'),
408
+ ]);
409
471
- ReactNoop.render(<Counter />);
472
- expect(Scheduler).toFlushAndYield([
473
- 'Render: 0',
474
- 'Render: 1',
475
- 'Render: 2',
476
- 'Render: 3',
477
- 3,
478
- ]);
479
- expect(ReactNoop.getChildren()).toEqual([span(3)]);
480
- });
410
+ ReactNoop.render(<ScrollView row={2} />);
411
+ expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
412
+ expect(ReactNoop.getChildren()).toEqual([
413
+ span('Scrolling down: false'),
414
+ ]);
415
482
- it('uses reducer passed at time of render, not time of dispatch', () => {
483
- // This test is a bit contrived but it demonstrates a subtle edge case.
416
+ ReactNoop.render(<ScrollView row={2} />);
417
+ expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
418
+ expect(ReactNoop.getChildren()).toEqual([
419
+ span('Scrolling down: false'),
420
+ ]);
421
+ });
422
485
- // Reducer A increments by 1. Reducer B increments by 10.
486
- function reducerA(state, action) {
487
- switch (action) {
488
- case 'increment':
489
- return state + 1;
490
- case 'reset':
491
- return 0;
492
- }
493
- }
494
- function reducerB(state, action) {
495
- switch (action) {
496
- case 'increment':
497
- return state + 10;
498
- case 'reset':
499
- return 0;
500
- }
501
- }
423
+ it('keeps restarting until there are no more new updates', () => {
424
+ function Counter({row: newRow}) {
425
+ let [count, setCount] = useState(0);
426
+ if (count < 3) {
427
+ setCount(count + 1);
428
+ }
429
+ Scheduler.unstable_yieldValue('Render: ' + count);
430
+ return <Text text={count} />;
431
+ }
432
503
- function Counter({row: newRow}, ref) {
504
- let [reducer, setReducer] = useState(() => reducerA);
505
- let [count, dispatch] = useReducer(reducer, 0);
506
- useImperativeHandle(ref, () => ({dispatch}));
507
- if (count < 20) {
508
- dispatch('increment');
509
- // Swap reducers each time we increment
510
- if (reducer === reducerA) {
511
- setReducer(() => reducerB);
512
- } else {
513
- setReducer(() => reducerA);
433
+ ReactNoop.render(<Counter />);
434
+ expect(Scheduler).toFlushAndYield([
435
+ 'Render: 0',
436
+ 'Render: 1',
437
+ 'Render: 2',
438
+ 'Render: 3',
439
+ 3,
440
+ ]);
441
+ expect(ReactNoop.getChildren()).toEqual([span(3)]);
442
+ });
443
+
444
+ it('updates multiple times within same render function', () => {
445
+ function Counter({row: newRow}) {
446
+ let [count, setCount] = useState(0);
447
+ if (count < 12) {
448
+ setCount(c => c + 1);
449
+ setCount(c => c + 1);
450
+ setCount(c => c + 1);
451
+ }
452
+ Scheduler.unstable_yieldValue('Render: ' + count);
453
+ return <Text text={count} />;
454
}
515
- }
516
- Scheduler.unstable_yieldValue('Render: ' + count);
517
- return <Text text={count} />;
518
- }
519
- Counter = forwardRef(Counter);
520
- const counter = React.createRef(null);
521
- ReactNoop.render(<Counter ref={counter} />);
522
- expect(Scheduler).toFlushAndYield([
523
- // The count should increase by alternating amounts of 10 and 1
524
- // until we reach 21.
525
- 'Render: 0',
526
- 'Render: 10',
527
- 'Render: 11',
528
- 'Render: 21',
529
- 21,
530
- ]);
531
- expect(ReactNoop.getChildren()).toEqual([span(21)]);
532
-
533
- // Test that it works on update, too. This time the log is a bit different
534
- // because we started with reducerB instead of reducerA.
535
- ReactNoop.act(() => {
536
- counter.current.dispatch('reset');
537
- });
538
- ReactNoop.render(<Counter ref={counter} />);
539
- expect(Scheduler).toHaveYielded([
540
- 'Render: 0',
541
- 'Render: 1',
542
- 'Render: 11',
543
- 'Render: 12',
544
- 'Render: 22',
545
- 22,
546
- ]);
547
- expect(ReactNoop.getChildren()).toEqual([span(22)]);
548
- });
455
550
- it('discards render phase updates if something suspends', () => {
551
- const thenable = {then() {}};
552
- function Foo({signal}) {
553
- return (
554
- <Suspense fallback="Loading...">
555
- <Bar signal={signal} />
556
- </Suspense>
557
- );
558
- }
456
+ ReactNoop.render(<Counter />);
457
+ expect(Scheduler).toFlushAndYield([
458
+ // Should increase by three each time
459
+ 'Render: 0',
460
+ 'Render: 3',
461
+ 'Render: 6',
462
+ 'Render: 9',
463
+ 'Render: 12',
464
+ 12,
465
+ ]);
466
+ expect(ReactNoop.getChildren()).toEqual([span(12)]);
467
+ });
468
560
- function Bar({signal: newSignal}) {
561
- let [counter, setCounter] = useState(0);
562
- let [signal, setSignal] = useState(true);
469
+ it('throws after too many iterations', () => {
470
+ function Counter({row: newRow}) {
471
+ let [count, setCount] = useState(0);
472
+ setCount(count + 1);
473
+ Scheduler.unstable_yieldValue('Render: ' + count);
474
+ return <Text text={count} />;
475
+ }
476
+ ReactNoop.render(<Counter />);
477
+ expect(Scheduler).toFlushAndThrow(
478
+ 'Too many re-renders. React limits the number of renders to prevent ' +
479
+ 'an infinite loop.',
480
+ );
481
+ });
482
564
- // Increment a counter every time the signal changes
565
- if (signal !== newSignal) {
566
- setCounter(c => c + 1);
567
- setSignal(newSignal);
568
- if (counter === 0) {
569
- // We're suspending during a render that includes render phase
570
- // updates. Those updates should not persist to the next render.
571
- Scheduler.unstable_yieldValue('Suspend!');
572
- throw thenable;
483
+ it('works with useReducer', () => {
484
+ function reducer(state, action) {
485
+ return action === 'increment' ? state + 1 : state;
486
+ }
487
+ function Counter({row: newRow}) {
488
+ let [count, dispatch] = useReducer(reducer, 0);
489
+ if (count < 3) {
490
+ dispatch('increment');
491
+ }
492
+ Scheduler.unstable_yieldValue('Render: ' + count);
493
+ return <Text text={count} />;
494
}
574
- }
495
576
- return <Text text={counter} />;
577
- }
496
+ ReactNoop.render(<Counter />);
497
+ expect(Scheduler).toFlushAndYield([
498
+ 'Render: 0',
499
+ 'Render: 1',
500
+ 'Render: 2',
501
+ 'Render: 3',
502
+ 3,
503
+ ]);
504
+ expect(ReactNoop.getChildren()).toEqual([span(3)]);
505
+ });
506
579
- const root = ReactNoop.createRoot();
580
- root.render(<Foo signal={true} />);
507
+ it('uses reducer passed at time of render, not time of dispatch', () => {
508
+ // This test is a bit contrived but it demonstrates a subtle edge case.
509
+
510
+ // Reducer A increments by 1. Reducer B increments by 10.
511
+ function reducerA(state, action) {
512
+ switch (action) {
513
+ case 'increment':
514
+ return state + 1;
515
+ case 'reset':
516
+ return 0;
517
+ }
518
+ }
519
+ function reducerB(state, action) {
520
+ switch (action) {
521
+ case 'increment':
522
+ return state + 10;
523
+ case 'reset':
524
+ return 0;
525
+ }
526
+ }
527
582
- expect(Scheduler).toFlushAndYield([0]);
583
- expect(root).toMatchRenderedOutput(<span prop={0} />);
528
+ function Counter({row: newRow}, ref) {
529
+ let [reducer, setReducer] = useState(() => reducerA);
530
+ let [count, dispatch] = useReducer(reducer, 0);
531
+ useImperativeHandle(ref, () => ({dispatch}));
532
+ if (count < 20) {
533
+ dispatch('increment');
534
+ // Swap reducers each time we increment
535
+ if (reducer === reducerA) {
536
+ setReducer(() => reducerB);
537
+ } else {
538
+ setReducer(() => reducerA);
539
+ }
540
+ }
541
+ Scheduler.unstable_yieldValue('Render: ' + count);
542
+ return <Text text={count} />;
543
+ }
544
+ Counter = forwardRef(Counter);
545
+ const counter = React.createRef(null);
546
+ ReactNoop.render(<Counter ref={counter} />);
547
+ expect(Scheduler).toFlushAndYield([
548
+ // The count should increase by alternating amounts of 10 and 1
549
+ // until we reach 21.
550
+ 'Render: 0',
551
+ 'Render: 10',
552
+ 'Render: 11',
553
+ 'Render: 21',
554
+ 21,
555
+ ]);
556
+ expect(ReactNoop.getChildren()).toEqual([span(21)]);
557
585
- root.render(<Foo signal={false} />);
586
- expect(Scheduler).toFlushAndYield(['Suspend!']);
587
- expect(root).toMatchRenderedOutput(<span prop={0} />);
558
+ // Test that it works on update, too. This time the log is a bit different
559
+ // because we started with reducerB instead of reducerA.
560
+ ReactNoop.act(() => {
561
+ counter.current.dispatch('reset');
562
+ });
563
+ ReactNoop.render(<Counter ref={counter} />);
564
+ expect(Scheduler).toHaveYielded([
565
+ 'Render: 0',
566
+ 'Render: 1',
567
+ 'Render: 11',
568
+ 'Render: 12',
569
+ 'Render: 22',
570
+ 22,
571
+ ]);
572
+ expect(ReactNoop.getChildren()).toEqual([span(22)]);
573
+ });
574
589
- // Rendering again should suspend again.
590
- root.render(<Foo signal={false} />);
591
- expect(Scheduler).toFlushAndYield(['Suspend!']);
592
- });
575
+ it('discards render phase updates if something suspends', () => {
576
+ const thenable = {then() {}};
577
+ function Foo({signal}) {
578
+ return (
579
+ <Suspense fallback="Loading...">
580
+ <Bar signal={signal} />
581
+ </Suspense>
582
+ );
583
+ }
584
594
- it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
595
- const thenable = {then() {}};
596
- function Foo({signal}) {
597
- return (
598
- <Suspense fallback="Loading...">
599
- <Bar signal={signal} />
600
- </Suspense>
601
- );
602
- }
585
+ function Bar({signal: newSignal}) {
586
+ let [counter, setCounter] = useState(0);
587
+ let [signal, setSignal] = useState(true);
588
+
589
+ // Increment a counter every time the signal changes
590
+ if (signal !== newSignal) {
591
+ setCounter(c => c + 1);
592
+ setSignal(newSignal);
593
+ if (counter === 0) {
594
+ // We're suspending during a render that includes render phase
595
+ // updates. Those updates should not persist to the next render.
596
+ Scheduler.unstable_yieldValue('Suspend!');
597
+ throw thenable;
598
+ }
599
+ }
600
+
601
+ return <Text text={counter} />;
602
+ }
603
604
- let setLabel;
605
- function Bar({signal: newSignal}) {
606
- let [counter, setCounter] = useState(0);
604
+ const root = ReactNoop.createRoot();
605
+ root.render(<Foo signal={true} />);
606
608
- if (counter === 1) {
609
- // We're suspending during a render that includes render phase
610
- // updates. Those updates should not persist to the next render.
611
- Scheduler.unstable_yieldValue('Suspend!');
612
- throw thenable;
613
- }
607
+ expect(Scheduler).toFlushAndYield([0]);
608
+ expect(root).toMatchRenderedOutput(<span prop={0} />);
609
615
- let [signal, setSignal] = useState(true);
610
+ root.render(<Foo signal={false} />);
611
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
612
+ expect(root).toMatchRenderedOutput(<span prop={0} />);
613
617
- // Increment a counter every time the signal changes
618
- if (signal !== newSignal) {
619
- setCounter(c => c + 1);
620
- setSignal(newSignal);
621
- }
614
+ // Rendering again should suspend again.
615
+ root.render(<Foo signal={false} />);
616
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
617
+ });
618
623
- let [label, _setLabel] = useState('A');
624
- setLabel = _setLabel;
619
+ it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
620
+ const thenable = {then() {}};
621
+ function Foo({signal}) {
622
+ return (
623
+ <Suspense fallback="Loading...">
624
+ <Bar signal={signal} />
625
+ </Suspense>
626
+ );
627
+ }
628
626
- return <Text text={`${label}:${counter}`} />;
627
- }
629
+ let setLabel;
630
+ function Bar({signal: newSignal}) {
631
+ let [counter, setCounter] = useState(0);
632
629
- const root = ReactNoop.createRoot();
630
- root.render(<Foo signal={true} />);
633
+ if (counter === 1) {
634
+ // We're suspending during a render that includes render phase
635
+ // updates. Those updates should not persist to the next render.
636
+ Scheduler.unstable_yieldValue('Suspend!');
637
+ throw thenable;
638
+ }
639
632
- expect(Scheduler).toFlushAndYield(['A:0']);
633
- expect(root).toMatchRenderedOutput(<span prop="A:0" />);
640
+ let [signal, setSignal] = useState(true);
641
635
- await ReactNoop.act(async () => {
636
- root.render(<Foo signal={false} />);
637
- setLabel('B');
638
- });
639
- expect(Scheduler).toHaveYielded(['Suspend!']);
640
- expect(root).toMatchRenderedOutput(<span prop="A:0" />);
641
-
642
- // Rendering again should suspend again.
643
- root.render(<Foo signal={false} />);
644
- expect(Scheduler).toFlushAndYield(['Suspend!']);
645
-
646
- // Flip the signal back to "cancel" the update. However, the update to
647
- // label should still proceed. It shouldn't have been dropped.
648
- root.render(<Foo signal={true} />);
649
- expect(Scheduler).toFlushAndYield(['B:0']);
650
- expect(root).toMatchRenderedOutput(<span prop="B:0" />);
651
- });
642
+ // Increment a counter every time the signal changes
643
+ if (signal !== newSignal) {
644
+ setCounter(c => c + 1);
645
+ setSignal(newSignal);
646
+ }
647
653
- // TODO: This should probably warn
654
- it.experimental('calling startTransition inside render phase', async () => {
655
- let startTransition;
656
- function App() {
657
- let [counter, setCounter] = useState(0);
658
- let [_startTransition] = useTransition();
659
- startTransition = _startTransition;
660
-
661
- if (counter === 0) {
662
- startTransition(() => {
663
- setCounter(c => c + 1);
664
- });
665
- }
648
+ let [label, _setLabel] = useState('A');
649
+ setLabel = _setLabel;
650
667
- return <Text text={counter} />;
668
- }
651
+ return <Text text={`${label}:${counter}`} />;
652
+ }
653
670
- const root = ReactNoop.createRoot();
671
- root.render(<App />);
672
- expect(Scheduler).toFlushAndYield([1]);
673
- expect(root).toMatchRenderedOutput(<span prop={1} />);
674
- });
675
- });
654
+ const root = ReactNoop.createRoot();
655
+ root.render(<Foo signal={true} />);
656
677
- describe('useReducer', () => {
678
- it('simple mount and update', () => {
679
- const INCREMENT = 'INCREMENT';
680
- const DECREMENT = 'DECREMENT';
681
-
682
- function reducer(state, action) {
683
- switch (action) {
684
- case 'INCREMENT':
685
- return state + 1;
686
- case 'DECREMENT':
687
- return state - 1;
688
- default:
689
- return state;
690
- }
691
- }
657
+ expect(Scheduler).toFlushAndYield(['A:0']);
658
+ expect(root).toMatchRenderedOutput(<span prop="A:0" />);
659
693
- function Counter(props, ref) {
694
- const [count, dispatch] = useReducer(reducer, 0);
695
- useImperativeHandle(ref, () => ({dispatch}));
696
- return <Text text={'Count: ' + count} />;
697
- }
698
- Counter = forwardRef(Counter);
699
- const counter = React.createRef(null);
700
- ReactNoop.render(<Counter ref={counter} />);
701
- expect(Scheduler).toFlushAndYield(['Count: 0']);
702
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
703
-
704
- act(() => counter.current.dispatch(INCREMENT));
705
- expect(Scheduler).toHaveYielded(['Count: 1']);
706
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
707
- act(() => {
708
- counter.current.dispatch(DECREMENT);
709
- counter.current.dispatch(DECREMENT);
710
- counter.current.dispatch(DECREMENT);
660
+ await ReactNoop.act(async () => {
661
+ root.render(<Foo signal={false} />);
662
+ setLabel('B');
663
+ });
664
+ expect(Scheduler).toHaveYielded(['Suspend!']);
665
+ expect(root).toMatchRenderedOutput(<span prop="A:0" />);
666
+
667
+ // Rendering again should suspend again.
668
+ root.render(<Foo signal={false} />);
669
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
670
+
671
+ // Flip the signal back to "cancel" the update. However, the update to
672
+ // label should still proceed. It shouldn't have been dropped.
673
+ root.render(<Foo signal={true} />);
674
+ expect(Scheduler).toFlushAndYield(['B:0']);
675
+ expect(root).toMatchRenderedOutput(<span prop="B:0" />);
676
+ });
677
+
678
+ // TODO: This should probably warn
679
+ it.experimental(
680
+ 'calling startTransition inside render phase',
681
+ async () => {
682
+ let startTransition;
683
+ function App() {
684
+ let [counter, setCounter] = useState(0);
685
+ let [_startTransition] = useTransition();
686
+ startTransition = _startTransition;
687
+
688
+ if (counter === 0) {
689
+ startTransition(() => {
690
+ setCounter(c => c + 1);
691
+ });
692
+ }
693
+
694
+ return <Text text={counter} />;
695
+ }
696
+
697
+ const root = ReactNoop.createRoot();
698
+ root.render(<App />);
699
+ expect(Scheduler).toFlushAndYield([1]);
700
+ expect(root).toMatchRenderedOutput(<span prop={1} />);
701
+ },
702
+ );
703
});
704
713
- expect(Scheduler).toHaveYielded(['Count: -2']);
714
- expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]);
715
- });
705
+ describe('useReducer', () => {
706
+ it('simple mount and update', () => {
707
+ const INCREMENT = 'INCREMENT';
708
+ const DECREMENT = 'DECREMENT';
709
+
710
+ function reducer(state, action) {
711
+ switch (action) {
712
+ case 'INCREMENT':
713
+ return state + 1;
714
+ case 'DECREMENT':
715
+ return state - 1;
716
+ default:
717
+ return state;
718
+ }
719
+ }
720
717
- it('lazy init', () => {
718
- const INCREMENT = 'INCREMENT';
719
- const DECREMENT = 'DECREMENT';
720
-
721
- function reducer(state, action) {
722
- switch (action) {
723
- case 'INCREMENT':
724
- return state + 1;
725
- case 'DECREMENT':
726
- return state - 1;
727
- default:
728
- return state;
729
- }
730
- }
721
+ function Counter(props, ref) {
722
+ const [count, dispatch] = useReducer(reducer, 0);
723
+ useImperativeHandle(ref, () => ({dispatch}));
724
+ return <Text text={'Count: ' + count} />;
725
+ }
726
+ Counter = forwardRef(Counter);
727
+ const counter = React.createRef(null);
728
+ ReactNoop.render(<Counter ref={counter} />);
729
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
730
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
731
+
732
+ act(() => counter.current.dispatch(INCREMENT));
733
+ expect(Scheduler).toHaveYielded(['Count: 1']);
734
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
735
+ act(() => {
736
+ counter.current.dispatch(DECREMENT);
737
+ counter.current.dispatch(DECREMENT);
738
+ counter.current.dispatch(DECREMENT);
739
+ });
740
732
- function Counter(props, ref) {
733
- const [count, dispatch] = useReducer(reducer, props, p => {
734
- Scheduler.unstable_yieldValue('Init');
735
- return p.initialCount;
741
+ expect(Scheduler).toHaveYielded(['Count: -2']);
742
+ expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]);
743
});
737
- useImperativeHandle(ref, () => ({dispatch}));
738
- return <Text text={'Count: ' + count} />;
739
- }
740
- Counter = forwardRef(Counter);
741
- const counter = React.createRef(null);
742
- ReactNoop.render(<Counter initialCount={10} ref={counter} />);
743
- expect(Scheduler).toFlushAndYield(['Init', 'Count: 10']);
744
- expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]);
745
-
746
- act(() => counter.current.dispatch(INCREMENT));
747
- expect(Scheduler).toHaveYielded(['Count: 11']);
748
- expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
749
-
750
- act(() => {
751
- counter.current.dispatch(DECREMENT);
752
- counter.current.dispatch(DECREMENT);
753
- counter.current.dispatch(DECREMENT);
754
- });
744
756
- expect(Scheduler).toHaveYielded(['Count: 8']);
757
- expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]);
758
- });
745
+ it('lazy init', () => {
746
+ const INCREMENT = 'INCREMENT';
747
+ const DECREMENT = 'DECREMENT';
748
+
749
+ function reducer(state, action) {
750
+ switch (action) {
751
+ case 'INCREMENT':
752
+ return state + 1;
753
+ case 'DECREMENT':
754
+ return state - 1;
755
+ default:
756
+ return state;
757
+ }
758
+ }
759
760
- // Regression test for https://github.com/facebook/react/issues/14360
761
- it('handles dispatches with mixed priorities', () => {
762
- const INCREMENT = 'INCREMENT';
760
+ function Counter(props, ref) {
761
+ const [count, dispatch] = useReducer(reducer, props, p => {
762
+ Scheduler.unstable_yieldValue('Init');
763
+ return p.initialCount;
764
+ });
765
+ useImperativeHandle(ref, () => ({dispatch}));
766
+ return <Text text={'Count: ' + count} />;
767
+ }
768
+ Counter = forwardRef(Counter);
769
+ const counter = React.createRef(null);
770
+ ReactNoop.render(<Counter initialCount={10} ref={counter} />);
771
+ expect(Scheduler).toFlushAndYield(['Init', 'Count: 10']);
772
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]);
773
+
774
+ act(() => counter.current.dispatch(INCREMENT));
775
+ expect(Scheduler).toHaveYielded(['Count: 11']);
776
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
777
+
778
+ act(() => {
779
+ counter.current.dispatch(DECREMENT);
780
+ counter.current.dispatch(DECREMENT);
781
+ counter.current.dispatch(DECREMENT);
782
+ });
783
764
- function reducer(state, action) {
765
- return action === INCREMENT ? state + 1 : state;
766
- }
784
+ expect(Scheduler).toHaveYielded(['Count: 8']);
785
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]);
786
+ });
787
768
- function Counter(props, ref) {
769
- const [count, dispatch] = useReducer(reducer, 0);
770
- useImperativeHandle(ref, () => ({dispatch}));
771
- return <Text text={'Count: ' + count} />;
772
- }
788
+ // Regression test for https://github.com/facebook/react/issues/14360
789
+ it('handles dispatches with mixed priorities', () => {
790
+ const INCREMENT = 'INCREMENT';
791
774
- Counter = forwardRef(Counter);
775
- const counter = React.createRef(null);
776
- ReactNoop.render(<Counter ref={counter} />);
792
+ function reducer(state, action) {
793
+ return action === INCREMENT ? state + 1 : state;
794
+ }
795
778
- expect(Scheduler).toFlushAndYield(['Count: 0']);
779
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
796
+ function Counter(props, ref) {
797
+ const [count, dispatch] = useReducer(reducer, 0);
798
+ useImperativeHandle(ref, () => ({dispatch}));
799
+ return <Text text={'Count: ' + count} />;
800
+ }
801
781
- ReactNoop.batchedUpdates(() => {
782
- counter.current.dispatch(INCREMENT);
783
- counter.current.dispatch(INCREMENT);
784
- counter.current.dispatch(INCREMENT);
785
- });
802
+ Counter = forwardRef(Counter);
803
+ const counter = React.createRef(null);
804
+ ReactNoop.render(<Counter ref={counter} />);
805
787
- ReactNoop.flushSync(() => {
788
- counter.current.dispatch(INCREMENT);
789
- });
790
- expect(Scheduler).toHaveYielded(['Count: 1']);
791
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
806
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
807
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
808
793
- expect(Scheduler).toFlushAndYield(['Count: 4']);
794
- expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
795
- });
796
- });
809
+ ReactNoop.batchedUpdates(() => {
810
+ counter.current.dispatch(INCREMENT);
811
+ counter.current.dispatch(INCREMENT);
812
+ counter.current.dispatch(INCREMENT);
813
+ });
814
+
815
+ ReactNoop.flushSync(() => {
816
+ counter.current.dispatch(INCREMENT);
817
+ });
818
+ expect(Scheduler).toHaveYielded(['Count: 1']);
819
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
820
798
- describe('useEffect', () => {
799
- it('simple mount and update', () => {
800
- function Counter(props) {
801
- useEffect(() => {
802
- Scheduler.unstable_yieldValue(`Passive effect [${props.count}]`);
821
+ expect(Scheduler).toFlushAndYield(['Count: 4']);
822
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
823
});
804
- return <Text text={'Count: ' + props.count} />;
805
- }
806
- act(() => {
807
- ReactNoop.render(<Counter count={0} />, () =>
808
- Scheduler.unstable_yieldValue('Sync effect'),
809
- );
810
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
811
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
812
- // Effects are deferred until after the commit
813
- expect(Scheduler).toFlushAndYield(['Passive effect [0]']);
824
});
825
816
- act(() => {
817
- ReactNoop.render(<Counter count={1} />, () =>
818
- Scheduler.unstable_yieldValue('Sync effect'),
819
- );
820
- expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
821
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
822
- // Effects are deferred until after the commit
823
- expect(Scheduler).toFlushAndYield(['Passive effect [1]']);
824
- });
825
- });
826
+ describe('useEffect', () => {
827
+ it('simple mount and update', () => {
828
+ function Counter(props) {
829
+ useEffect(() => {
830
+ Scheduler.unstable_yieldValue(`Passive effect [${props.count}]`);
831
+ });
832
+ return <Text text={'Count: ' + props.count} />;
833
+ }
834
+ act(() => {
835
+ ReactNoop.render(<Counter count={0} />, () =>
836
+ Scheduler.unstable_yieldValue('Sync effect'),
837
+ );
838
+ expect(Scheduler).toFlushAndYieldThrough([
839
+ 'Count: 0',
840
+ 'Sync effect',
841
+ ]);
842
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
843
+ // Effects are deferred until after the commit
844
+ expect(Scheduler).toFlushAndYield(['Passive effect [0]']);
845
+ });
846
827
- it('flushes passive effects even with sibling deletions', () => {
828
- function LayoutEffect(props) {
829
- useLayoutEffect(() => {
830
- Scheduler.unstable_yieldValue(`Layout effect`);
847
+ act(() => {
848
+ ReactNoop.render(<Counter count={1} />, () =>
849
+ Scheduler.unstable_yieldValue('Sync effect'),
850
+ );
851
+ expect(Scheduler).toFlushAndYieldThrough([
852
+ 'Count: 1',
853
+ 'Sync effect',
854
+ ]);
855
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
856
+ // Effects are deferred until after the commit
857
+ expect(Scheduler).toFlushAndYield(['Passive effect [1]']);
858
+ });
859
});
832
- return <Text text="Layout" />;
833
- }
834
- function PassiveEffect(props) {
835
- useEffect(() => {
836
- Scheduler.unstable_yieldValue(`Passive effect`);
837
- }, []);
838
- return <Text text="Passive" />;
839
- }
840
- let passive = <PassiveEffect key="p" />;
841
- act(() => {
842
- ReactNoop.render([<LayoutEffect key="l" />, passive]);
843
- expect(Scheduler).toFlushAndYieldThrough([
844
- 'Layout',
845
- 'Passive',
846
- 'Layout effect',
847
- ]);
848
- expect(ReactNoop.getChildren()).toEqual([
849
- span('Layout'),
850
- span('Passive'),
851
- ]);
852
- // Destroying the first child shouldn't prevent the passive effect from
853
- // being executed
854
- ReactNoop.render([passive]);
855
- expect(Scheduler).toFlushAndYield(['Passive effect']);
856
- expect(ReactNoop.getChildren()).toEqual([span('Passive')]);
857
- });
858
- // exiting act calls flushPassiveEffects(), but there are none left to flush.
859
- expect(Scheduler).toHaveYielded([]);
860
- });
860
862
- it('flushes passive effects even if siblings schedule an update', () => {
863
- function PassiveEffect(props) {
864
- useEffect(() => {
865
- Scheduler.unstable_yieldValue('Passive effect');
866
- });
867
- return <Text text="Passive" />;
868
- }
869
- function LayoutEffect(props) {
870
- let [count, setCount] = useState(0);
871
- useLayoutEffect(() => {
872
- // Scheduling work shouldn't interfere with the queued passive effect
873
- if (count === 0) {
874
- setCount(1);
875
- }
876
- Scheduler.unstable_yieldValue('Layout effect ' + count);
861
+ it('flushes passive effects even with sibling deletions', () => {
862
+ function LayoutEffect(props) {
863
+ useLayoutEffect(() => {
864
+ Scheduler.unstable_yieldValue(`Layout effect`);
865
+ });
866
+ return <Text text="Layout" />;
867
+ }
868
+ function PassiveEffect(props) {
869
+ useEffect(() => {
870
+ Scheduler.unstable_yieldValue(`Passive effect`);
871
+ }, []);
872
+ return <Text text="Passive" />;
873
+ }
874
+ let passive = <PassiveEffect key="p" />;
875
+ act(() => {
876
+ ReactNoop.render([<LayoutEffect key="l" />, passive]);
877
+ expect(Scheduler).toFlushAndYieldThrough([
878
+ 'Layout',
879
+ 'Passive',
880
+ 'Layout effect',
881
+ ]);
882
+ expect(ReactNoop.getChildren()).toEqual([
883
+ span('Layout'),
884
+ span('Passive'),
885
+ ]);
886
+ // Destroying the first child shouldn't prevent the passive effect from
887
+ // being executed
888
+ ReactNoop.render([passive]);
889
+ expect(Scheduler).toFlushAndYield(['Passive effect']);
890
+ expect(ReactNoop.getChildren()).toEqual([span('Passive')]);
891
+ });
892
+ // exiting act calls flushPassiveEffects(), but there are none left to flush.
893
+ expect(Scheduler).toHaveYielded([]);
894
});
878
- return <Text text="Layout" />;
879
- }
895
881
- ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);
896
+ it('flushes passive effects even if siblings schedule an update', () => {
897
+ function PassiveEffect(props) {
898
+ useEffect(() => {
899
+ Scheduler.unstable_yieldValue('Passive effect');
900
+ });
901
+ return <Text text="Passive" />;
902
+ }
903
+ function LayoutEffect(props) {
904
+ let [count, setCount] = useState(0);
905
+ useLayoutEffect(() => {
906
+ // Scheduling work shouldn't interfere with the queued passive effect
907
+ if (count === 0) {
908
+ setCount(1);
909
+ }
910
+ Scheduler.unstable_yieldValue('Layout effect ' + count);
911
+ });
912
+ return <Text text="Layout" />;
913
+ }
914
883
- act(() => {
884
- expect(Scheduler).toFlushAndYield([
885
- 'Passive',
886
- 'Layout',
887
- 'Layout effect 0',
888
- 'Passive effect',
889
- 'Layout',
890
- 'Layout effect 1',
891
- ]);
892
- });
915
+ ReactNoop.render([
916
+ <PassiveEffect key="p" />,
917
+ <LayoutEffect key="l" />,
918
+ ]);
919
894
- expect(ReactNoop.getChildren()).toEqual([
895
- span('Passive'),
896
- span('Layout'),
897
- ]);
898
- });
920
+ act(() => {
921
+ expect(Scheduler).toFlushAndYield([
922
+ 'Passive',
923
+ 'Layout',
924
+ 'Layout effect 0',
925
+ 'Passive effect',
926
+ 'Layout',
927
+ 'Layout effect 1',
928
+ ]);
929
+ });
930
900
- it('flushes passive effects even if siblings schedule a new root', () => {
901
- function PassiveEffect(props) {
902
- useEffect(() => {
903
- Scheduler.unstable_yieldValue('Passive effect');
904
- }, []);
905
- return <Text text="Passive" />;
906
- }
907
- function LayoutEffect(props) {
908
- useLayoutEffect(() => {
909
- Scheduler.unstable_yieldValue('Layout effect');
910
- // Scheduling work shouldn't interfere with the queued passive effect
911
- ReactNoop.renderToRootWithID(<Text text="New Root" />, 'root2');
931
+ expect(ReactNoop.getChildren()).toEqual([
932
+ span('Passive'),
933
+ span('Layout'),
934
+ ]);
935
});
913
- return <Text text="Layout" />;
914
- }
915
- act(() => {
916
- ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);
917
- expect(Scheduler).toFlushAndYield([
918
- 'Passive',
919
- 'Layout',
920
- 'Layout effect',
921
- 'Passive effect',
922
- 'New Root',
923
- ]);
924
- expect(ReactNoop.getChildren()).toEqual([
925
- span('Passive'),
926
- span('Layout'),
927
- ]);
928
- });
929
- });
936
931
- it(
932
- 'flushes effects serially by flushing old effects before flushing ' +
933
- "new ones, if they haven't already fired",
934
- () => {
935
- function getCommittedText() {
936
- const children = ReactNoop.getChildren();
937
- if (children === null) {
938
- return null;
937
+ it('flushes passive effects even if siblings schedule a new root', () => {
938
+ function PassiveEffect(props) {
939
+ useEffect(() => {
940
+ Scheduler.unstable_yieldValue('Passive effect');
941
+ }, []);
942
+ return <Text text="Passive" />;
943
}
940
- return children[0].prop;
941
- }
942
-
943
- function Counter(props) {
944
- useEffect(() => {
945
- Scheduler.unstable_yieldValue(
946
- `Committed state when effect was fired: ${getCommittedText()}`,
947
- );
944
+ function LayoutEffect(props) {
945
+ useLayoutEffect(() => {
946
+ Scheduler.unstable_yieldValue('Layout effect');
947
+ // Scheduling work shouldn't interfere with the queued passive effect
948
+ ReactNoop.renderToRootWithID(<Text text="New Root" />, 'root2');
949
+ });
950
+ return <Text text="Layout" />;
951
+ }
952
+ act(() => {
953
+ ReactNoop.render([
954
+ <PassiveEffect key="p" />,
955
+ <LayoutEffect key="l" />,
956
+ ]);
957
+ expect(Scheduler).toFlushAndYield([
958
+ 'Passive',
959
+ 'Layout',
960
+ 'Layout effect',
961
+ 'Passive effect',
962
+ 'New Root',
963
+ ]);
964
+ expect(ReactNoop.getChildren()).toEqual([
965
+ span('Passive'),
966
+ span('Layout'),
967
+ ]);
968
});
949
- return <Text text={props.count} />;
950
- }
951
- act(() => {
952
- ReactNoop.render(<Counter count={0} />, () =>
953
- Scheduler.unstable_yieldValue('Sync effect'),
954
- );
955
- expect(Scheduler).toFlushAndYieldThrough([0, 'Sync effect']);
956
- expect(ReactNoop.getChildren()).toEqual([span(0)]);
957
- // Before the effects have a chance to flush, schedule another update
958
- ReactNoop.render(<Counter count={1} />, () =>
959
- Scheduler.unstable_yieldValue('Sync effect'),
960
- );
961
- expect(Scheduler).toFlushAndYieldThrough([
962
- // The previous effect flushes before the reconciliation
963
- 'Committed state when effect was fired: 0',
964
- 1,
965
- 'Sync effect',
966
- ]);
967
- expect(ReactNoop.getChildren()).toEqual([span(1)]);
969
});
970
970
- expect(Scheduler).toHaveYielded([
971
- 'Committed state when effect was fired: 1',
972
- ]);
973
- },
974
- );
975
-
976
- it('defers passive effect destroy functions during unmount', () => {
977
- function Child({bar, foo}) {
978
- React.useEffect(() => {
979
- Scheduler.unstable_yieldValue('passive bar create');
980
- return () => {
981
- Scheduler.unstable_yieldValue('passive bar destroy');
982
- };
983
- }, [bar]);
984
- React.useLayoutEffect(() => {
985
- Scheduler.unstable_yieldValue('layout bar create');
986
- return () => {
987
- Scheduler.unstable_yieldValue('layout bar destroy');
988
- };
989
- }, [bar]);
990
- React.useEffect(() => {
991
- Scheduler.unstable_yieldValue('passive foo create');
992
- return () => {
993
- Scheduler.unstable_yieldValue('passive foo destroy');
994
- };
995
- }, [foo]);
996
- React.useLayoutEffect(() => {
997
- Scheduler.unstable_yieldValue('layout foo create');
998
- return () => {
999
- Scheduler.unstable_yieldValue('layout foo destroy');
1000
- };
1001
- }, [foo]);
1002
- Scheduler.unstable_yieldValue('render');
1003
- return null;
1004
- }
971
+ it(
972
+ 'flushes effects serially by flushing old effects before flushing ' +
973
+ "new ones, if they haven't already fired",
974
+ () => {
975
+ function getCommittedText() {
976
+ const children = ReactNoop.getChildren();
977
+ if (children === null) {
978
+ return null;
979
+ }
980
+ return children[0].prop;
981
+ }
982
+
983
+ function Counter(props) {
984
+ useEffect(() => {
985
+ Scheduler.unstable_yieldValue(
986
+ `Committed state when effect was fired: ${getCommittedText()}`,
987
+ );
988
+ });
989
+ return <Text text={props.count} />;
990
+ }
991
+ act(() => {
992
+ ReactNoop.render(<Counter count={0} />, () =>
993
+ Scheduler.unstable_yieldValue('Sync effect'),
994
+ );
995
+ expect(Scheduler).toFlushAndYieldThrough([0, 'Sync effect']);
996
+ expect(ReactNoop.getChildren()).toEqual([span(0)]);
997
+ // Before the effects have a chance to flush, schedule another update
998
+ ReactNoop.render(<Counter count={1} />, () =>
999
+ Scheduler.unstable_yieldValue('Sync effect'),
1000
+ );
1001
+ expect(Scheduler).toFlushAndYieldThrough([
1002
+ // The previous effect flushes before the reconciliation
1003
+ 'Committed state when effect was fired: 0',
1004
+ 1,
1005
+ 'Sync effect',
1006
+ ]);
1007
+ expect(ReactNoop.getChildren()).toEqual([span(1)]);
1008
+ });
1009
1006
- act(() => {
1007
- ReactNoop.render(<Child bar={1} foo={1} />, () =>
1008
- Scheduler.unstable_yieldValue('Sync effect'),
1010
+ expect(Scheduler).toHaveYielded([
1011
+ 'Committed state when effect was fired: 1',
1012
+ ]);
1013
+ },
1014
);
1010
- expect(Scheduler).toFlushAndYieldThrough([
1011
- 'render',
1012
- 'layout bar create',
1013
- 'layout foo create',
1014
- 'Sync effect',
1015
- ]);
1016
- // Effects are deferred until after the commit
1017
- expect(Scheduler).toFlushAndYield([
1018
- 'passive bar create',
1019
- 'passive foo create',
1020
- ]);
1021
- });
1022
-
1023
- // This update is exists to test an internal implementation detail:
1024
- // Effects without updating dependencies lose their layout/passive tag during an update.
1025
- act(() => {
1026
- ReactNoop.render(<Child bar={1} foo={2} />, () =>
1027
- Scheduler.unstable_yieldValue('Sync effect'),
1028
- );
1029
- expect(Scheduler).toFlushAndYieldThrough([
1030
- 'render',
1031
- 'layout foo destroy',
1032
- 'layout foo create',
1033
- 'Sync effect',
1034
- ]);
1035
- // Effects are deferred until after the commit
1036
- expect(Scheduler).toFlushAndYield([
1037
- 'passive foo destroy',
1038
- 'passive foo create',
1039
- ]);
1040
- });
1015
1042
- // Unmount the component and verify that passive destroy functions are deferred until post-commit.
1043
- act(() => {
1044
- ReactNoop.render(null, () =>
1045
- Scheduler.unstable_yieldValue('Sync effect'),
1046
- );
1047
- expect(Scheduler).toFlushAndYieldThrough([
1048
- 'layout bar destroy',
1049
- 'layout foo destroy',
1050
- 'Sync effect',
1051
- ]);
1052
- // Effects are deferred until after the commit
1053
- expect(Scheduler).toFlushAndYield([
1054
- 'passive bar destroy',
1055
- 'passive foo destroy',
1056
- ]);
1057
- });
1058
- });
1016
+ if (
1017
+ deferPassiveEffectCleanupDuringUnmount &&
1018
+ runAllPassiveEffectDestroysBeforeCreates
1019
+ ) {
1020
+ it('defers passive effect destroy functions during unmount', () => {
1021
+ function Child({bar, foo}) {
1022
+ React.useEffect(() => {
1023
+ Scheduler.unstable_yieldValue('passive bar create');
1024
+ return () => {
1025
+ Scheduler.unstable_yieldValue('passive bar destroy');
1026
+ };
1027
+ }, [bar]);
1028
+ React.useLayoutEffect(() => {
1029
+ Scheduler.unstable_yieldValue('layout bar create');
1030
+ return () => {
1031
+ Scheduler.unstable_yieldValue('layout bar destroy');
1032
+ };
1033
+ }, [bar]);
1034
+ React.useEffect(() => {
1035
+ Scheduler.unstable_yieldValue('passive foo create');
1036
+ return () => {
1037
+ Scheduler.unstable_yieldValue('passive foo destroy');
1038
+ };
1039
+ }, [foo]);
1040
+ React.useLayoutEffect(() => {
1041
+ Scheduler.unstable_yieldValue('layout foo create');
1042
+ return () => {
1043
+ Scheduler.unstable_yieldValue('layout foo destroy');
1044
+ };
1045
+ }, [foo]);
1046
+ Scheduler.unstable_yieldValue('render');
1047
+ return null;
1048
+ }
1049
+
1050
+ act(() => {
1051
+ ReactNoop.render(<Child bar={1} foo={1} />, () =>
1052
+ Scheduler.unstable_yieldValue('Sync effect'),
1053
+ );
1054
+ expect(Scheduler).toFlushAndYieldThrough([
1055
+ 'render',
1056
+ 'layout bar create',
1057
+ 'layout foo create',
1058
+ 'Sync effect',
1059
+ ]);
1060
+ // Effects are deferred until after the commit
1061
+ expect(Scheduler).toFlushAndYield([
1062
+ 'passive bar create',
1063
+ 'passive foo create',
1064
+ ]);
1065
+ });
1066
1060
- it('updates have async priority', () => {
1061
- function Counter(props) {
1062
- const [count, updateCount] = useState('(empty)');
1063
- useEffect(() => {
1064
- Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1065
- updateCount(props.count);
1066
- }, [props.count]);
1067
- return <Text text={'Count: ' + count} />;
1068
- }
1069
- act(() => {
1070
- ReactNoop.render(<Counter count={0} />, () =>
1071
- Scheduler.unstable_yieldValue('Sync effect'),
1072
- );
1073
- expect(Scheduler).toFlushAndYieldThrough([
1074
- 'Count: (empty)',
1075
- 'Sync effect',
1076
- ]);
1077
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1078
- ReactNoop.flushPassiveEffects();
1079
- expect(Scheduler).toHaveYielded(['Schedule update [0]']);
1080
- expect(Scheduler).toFlushAndYield(['Count: 0']);
1081
- });
1067
+ // This update is exists to test an internal implementation detail:
1068
+ // Effects without updating dependencies lose their layout/passive tag during an update.
1069
+ act(() => {
1070
+ ReactNoop.render(<Child bar={1} foo={2} />, () =>
1071
+ Scheduler.unstable_yieldValue('Sync effect'),
1072
+ );
1073
+ expect(Scheduler).toFlushAndYieldThrough([
1074
+ 'render',
1075
+ 'layout foo destroy',
1076
+ 'layout foo create',
1077
+ 'Sync effect',
1078
+ ]);
1079
+ // Effects are deferred until after the commit
1080
+ expect(Scheduler).toFlushAndYield([
1081
+ 'passive foo destroy',
1082
+ 'passive foo create',
1083
+ ]);
1084
+ });
1085
1083
- act(() => {
1084
- ReactNoop.render(<Counter count={1} />, () =>
1085
- Scheduler.unstable_yieldValue('Sync effect'),
1086
- );
1087
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1088
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1089
- ReactNoop.flushPassiveEffects();
1090
- expect(Scheduler).toHaveYielded(['Schedule update [1]']);
1091
- expect(Scheduler).toFlushAndYield(['Count: 1']);
1092
- });
1093
- });
1086
+ // Unmount the component and verify that passive destroy functions are deferred until post-commit.
1087
+ act(() => {
1088
+ ReactNoop.render(null, () =>
1089
+ Scheduler.unstable_yieldValue('Sync effect'),
1090
+ );
1091
+ expect(Scheduler).toFlushAndYieldThrough([
1092
+ 'layout bar destroy',
1093
+ 'layout foo destroy',
1094
+ 'Sync effect',
1095
+ ]);
1096
+ // Effects are deferred until after the commit
1097
+ expect(Scheduler).toFlushAndYield([
1098
+ 'passive bar destroy',
1099
+ 'passive foo destroy',
1100
+ ]);
1101
+ });
1102
+ });
1103
+ }
1104
1095
- it('updates have async priority even if effects are flushed early', () => {
1096
- function Counter(props) {
1097
- const [count, updateCount] = useState('(empty)');
1098
- useEffect(() => {
1099
- Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1100
- updateCount(props.count);
1101
- }, [props.count]);
1102
- return <Text text={'Count: ' + count} />;
1103
- }
1104
- act(() => {
1105
- ReactNoop.render(<Counter count={0} />, () =>
1106
- Scheduler.unstable_yieldValue('Sync effect'),
1107
- );
1108
- expect(Scheduler).toFlushAndYieldThrough([
1109
- 'Count: (empty)',
1110
- 'Sync effect',
1111
- ]);
1112
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1105
+ it('updates have async priority', () => {
1106
+ function Counter(props) {
1107
+ const [count, updateCount] = useState('(empty)');
1108
+ useEffect(() => {
1109
+ Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1110
+ updateCount(props.count);
1111
+ }, [props.count]);
1112
+ return <Text text={'Count: ' + count} />;
1113
+ }
1114
+ act(() => {
1115
+ ReactNoop.render(<Counter count={0} />, () =>
1116
+ Scheduler.unstable_yieldValue('Sync effect'),
1117
+ );
1118
+ expect(Scheduler).toFlushAndYieldThrough([
1119
+ 'Count: (empty)',
1120
+ 'Sync effect',
1121
+ ]);
1122
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1123
+ ReactNoop.flushPassiveEffects();
1124
+ expect(Scheduler).toHaveYielded(['Schedule update [0]']);
1125
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
1126
+ });
1127
1114
- // Rendering again should flush the previous commit's effects
1115
- ReactNoop.render(<Counter count={1} />, () =>
1116
- Scheduler.unstable_yieldValue('Sync effect'),
1117
- );
1118
- expect(Scheduler).toFlushAndYieldThrough([
1119
- 'Schedule update [0]',
1120
- 'Count: 0',
1121
- ]);
1122
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1128
+ act(() => {
1129
+ ReactNoop.render(<Counter count={1} />, () =>
1130
+ Scheduler.unstable_yieldValue('Sync effect'),
1131
+ );
1132
+ expect(Scheduler).toFlushAndYieldThrough([
1133
+ 'Count: 0',
1134
+ 'Sync effect',
1135
+ ]);
1136
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1137
+ ReactNoop.flushPassiveEffects();
1138
+ expect(Scheduler).toHaveYielded(['Schedule update [1]']);
1139
+ expect(Scheduler).toFlushAndYield(['Count: 1']);
1140
+ });
1141
+ });
1142
1124
- expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
1125
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1126
- ReactNoop.flushPassiveEffects();
1127
- expect(Scheduler).toHaveYielded(['Schedule update [1]']);
1128
- expect(Scheduler).toFlushAndYield(['Count: 1']);
1129
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1130
- });
1131
- });
1143
+ it('updates have async priority even if effects are flushed early', () => {
1144
+ function Counter(props) {
1145
+ const [count, updateCount] = useState('(empty)');
1146
+ useEffect(() => {
1147
+ Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1148
+ updateCount(props.count);
1149
+ }, [props.count]);
1150
+ return <Text text={'Count: ' + count} />;
1151
+ }
1152
+ act(() => {
1153
+ ReactNoop.render(<Counter count={0} />, () =>
1154
+ Scheduler.unstable_yieldValue('Sync effect'),
1155
+ );
1156
+ expect(Scheduler).toFlushAndYieldThrough([
1157
+ 'Count: (empty)',
1158
+ 'Sync effect',
1159
+ ]);
1160
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1161
+
1162
+ // Rendering again should flush the previous commit's effects
1163
+ ReactNoop.render(<Counter count={1} />, () =>
1164
+ Scheduler.unstable_yieldValue('Sync effect'),
1165
+ );
1166
+ expect(Scheduler).toFlushAndYieldThrough([
1167
+ 'Schedule update [0]',
1168
+ 'Count: 0',
1169
+ ]);
1170
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1171
+
1172
+ expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
1173
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1174
+ ReactNoop.flushPassiveEffects();
1175
+ expect(Scheduler).toHaveYielded(['Schedule update [1]']);
1176
+ expect(Scheduler).toFlushAndYield(['Count: 1']);
1177
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1178
+ });
1179
+ });
1180
1133
- it('flushes passive effects when flushing discrete updates', () => {
1134
- let _updateCount;
1135
- function Counter(props) {
1136
- const [count, updateCount] = useState(0);
1137
- _updateCount = updateCount;
1138
- useEffect(() => {
1139
- Scheduler.unstable_yieldValue(`Will set count to 1`);
1140
- updateCount(1);
1141
- }, []);
1142
- return <Text text={'Count: ' + count} />;
1143
- }
1181
+ it('flushes passive effects when flushing discrete updates', () => {
1182
+ let _updateCount;
1183
+ function Counter(props) {
1184
+ const [count, updateCount] = useState(0);
1185
+ _updateCount = updateCount;
1186
+ useEffect(() => {
1187
+ Scheduler.unstable_yieldValue(`Will set count to 1`);
1188
+ updateCount(1);
1189
+ }, []);
1190
+ return <Text text={'Count: ' + count} />;
1191
+ }
1192
1145
- // we explicitly wait for missing act() warnings here since
1146
- // it's a lot harder to simulate this condition inside an act scope
1147
- expect(() => {
1148
- ReactNoop.render(<Counter count={0} />, () =>
1149
- Scheduler.unstable_yieldValue('Sync effect'),
1150
- );
1151
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1152
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1153
- }).toErrorDev(['An update to Counter ran an effect']);
1193
+ // we explicitly wait for missing act() warnings here since
1194
+ // it's a lot harder to simulate this condition inside an act scope
1195
+ expect(() => {
1196
+ ReactNoop.render(<Counter count={0} />, () =>
1197
+ Scheduler.unstable_yieldValue('Sync effect'),
1198
+ );
1199
+ expect(Scheduler).toFlushAndYieldThrough([
1200
+ 'Count: 0',
1201
+ 'Sync effect',
1202
+ ]);
1203
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1204
+ }).toErrorDev(['An update to Counter ran an effect']);
1205
+
1206
+ // A discrete event forces the passive effect to be flushed --
1207
+ // updateCount(1) happens first, so 2 wins.
1208
+ ReactNoop.flushDiscreteUpdates();
1209
+ ReactNoop.discreteUpdates(() => {
1210
+ // (use batchedUpdates to silence the act() warning)
1211
+ ReactNoop.batchedUpdates(() => {
1212
+ _updateCount(2);
1213
+ });
1214
+ });
1215
+ expect(Scheduler).toHaveYielded(['Will set count to 1']);
1216
+ expect(() => {
1217
+ expect(Scheduler).toFlushAndYield(['Count: 2']);
1218
+ }).toErrorDev([
1219
+ 'An update to Counter ran an effect',
1220
+ 'An update to Counter ran an effect',
1221
+ ]);
1222
1155
- // A discrete event forces the passive effect to be flushed --
1156
- // updateCount(1) happens first, so 2 wins.
1157
- ReactNoop.flushDiscreteUpdates();
1158
- ReactNoop.discreteUpdates(() => {
1159
- // (use batchedUpdates to silence the act() warning)
1160
- ReactNoop.batchedUpdates(() => {
1161
- _updateCount(2);
1223
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
1224
});
1163
- });
1164
- expect(Scheduler).toHaveYielded(['Will set count to 1']);
1165
- expect(() => {
1166
- expect(Scheduler).toFlushAndYield(['Count: 2']);
1167
- }).toErrorDev([
1168
- 'An update to Counter ran an effect',
1169
- 'An update to Counter ran an effect',
1170
- ]);
1171
-
1172
- expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
1173
- });
1225
1175
- it('flushes passive effects when flushing discrete updates (with tracing)', () => {
1176
- const onInteractionScheduledWorkCompleted = jest.fn();
1177
- const onWorkCanceled = jest.fn();
1178
- SchedulerTracing.unstable_subscribe({
1179
- onInteractionScheduledWorkCompleted,
1180
- onInteractionTraced: jest.fn(),
1181
- onWorkCanceled,
1182
- onWorkScheduled: jest.fn(),
1183
- onWorkStarted: jest.fn(),
1184
- onWorkStopped: jest.fn(),
1185
- });
1226
+ it('flushes passive effects when flushing discrete updates (with tracing)', () => {
1227
+ const onInteractionScheduledWorkCompleted = jest.fn();
1228
+ const onWorkCanceled = jest.fn();
1229
+ SchedulerTracing.unstable_subscribe({
1230
+ onInteractionScheduledWorkCompleted,
1231
+ onInteractionTraced: jest.fn(),
1232
+ onWorkCanceled,
1233
+ onWorkScheduled: jest.fn(),
1234
+ onWorkStarted: jest.fn(),
1235
+ onWorkStopped: jest.fn(),
1236
+ });
1237
1187
- let _updateCount;
1188
- function Counter(props) {
1189
- const [count, updateCount] = useState(0);
1190
- _updateCount = updateCount;
1191
- useEffect(() => {
1192
- expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([
1193
- tracingEvent,
1194
- ]);
1195
- Scheduler.unstable_yieldValue(`Will set count to 1`);
1196
- updateCount(1);
1197
- }, []);
1198
- return <Text text={'Count: ' + count} />;
1199
- }
1238
+ let _updateCount;
1239
+ function Counter(props) {
1240
+ const [count, updateCount] = useState(0);
1241
+ _updateCount = updateCount;
1242
+ useEffect(() => {
1243
+ expect(
1244
+ SchedulerTracing.unstable_getCurrent(),
1245
+ ).toMatchInteractions([tracingEvent]);
1246
+ Scheduler.unstable_yieldValue(`Will set count to 1`);
1247
+ updateCount(1);
1248
+ }, []);
1249
+ return <Text text={'Count: ' + count} />;
1250
+ }
1251
1201
- const tracingEvent = {id: 0, name: 'hello', timestamp: 0};
1202
- // we explicitly wait for missing act() warnings here since
1203
- // it's a lot harder to simulate this condition inside an act scope
1204
- expect(() => {
1205
- SchedulerTracing.unstable_trace(
1206
- tracingEvent.name,
1207
- tracingEvent.timestamp,
1208
- () => {
1209
- ReactNoop.render(<Counter count={0} />, () =>
1210
- Scheduler.unstable_yieldValue('Sync effect'),
1252
+ const tracingEvent = {id: 0, name: 'hello', timestamp: 0};
1253
+ // we explicitly wait for missing act() warnings here since
1254
+ // it's a lot harder to simulate this condition inside an act scope
1255
+ expect(() => {
1256
+ SchedulerTracing.unstable_trace(
1257
+ tracingEvent.name,
1258
+ tracingEvent.timestamp,
1259
+ () => {
1260
+ ReactNoop.render(<Counter count={0} />, () =>
1261
+ Scheduler.unstable_yieldValue('Sync effect'),
1262
+ );
1263
+ },
1264
);
1212
- },
1213
- );
1214
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1215
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1216
- }).toErrorDev(['An update to Counter ran an effect']);
1265
+ expect(Scheduler).toFlushAndYieldThrough([
1266
+ 'Count: 0',
1267
+ 'Sync effect',
1268
+ ]);
1269
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1270
+ }).toErrorDev(['An update to Counter ran an effect']);
1271
+
1272
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0);
1273
+
1274
+ // A discrete event forces the passive effect to be flushed --
1275
+ // updateCount(1) happens first, so 2 wins.
1276
+ ReactNoop.flushDiscreteUpdates();
1277
+ ReactNoop.discreteUpdates(() => {
1278
+ // (use batchedUpdates to silence the act() warning)
1279
+ ReactNoop.batchedUpdates(() => {
1280
+ _updateCount(2);
1281
+ });
1282
+ });
1283
+ expect(Scheduler).toHaveYielded(['Will set count to 1']);
1284
+ expect(() => {
1285
+ expect(Scheduler).toFlushAndYield(['Count: 2']);
1286
+ }).toErrorDev([
1287
+ 'An update to Counter ran an effect',
1288
+ 'An update to Counter ran an effect',
1289
+ ]);
1290
1218
- expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0);
1291
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
1292
1220
- // A discrete event forces the passive effect to be flushed --
1221
- // updateCount(1) happens first, so 2 wins.
1222
- ReactNoop.flushDiscreteUpdates();
1223
- ReactNoop.discreteUpdates(() => {
1224
- // (use batchedUpdates to silence the act() warning)
1225
- ReactNoop.batchedUpdates(() => {
1226
- _updateCount(2);
1293
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1294
+ expect(onWorkCanceled).toHaveBeenCalledTimes(0);
1295
});
1228
- });
1229
- expect(Scheduler).toHaveYielded(['Will set count to 1']);
1230
- expect(() => {
1231
- expect(Scheduler).toFlushAndYield(['Count: 2']);
1232
- }).toErrorDev([
1233
- 'An update to Counter ran an effect',
1234
- 'An update to Counter ran an effect',
1235
- ]);
1236
-
1237
- expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
1238
-
1239
- expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1240
- expect(onWorkCanceled).toHaveBeenCalledTimes(0);
1241
- });
1296
1243
- it(
1244
- 'in legacy mode, useEffect is deferred and updates finish synchronously ' +
1245
- '(in a single batch)',
1246
- () => {
1247
- function Counter(props) {
1248
- const [count, updateCount] = useState('(empty)');
1249
- useEffect(() => {
1250
- // Update multiple times. These should all be batched together in
1251
- // a single render.
1252
- updateCount(props.count);
1253
- updateCount(props.count);
1254
- updateCount(props.count);
1255
- updateCount(props.count);
1256
- updateCount(props.count);
1257
- updateCount(props.count);
1258
- }, [props.count]);
1259
- return <Text text={'Count: ' + count} />;
1260
- }
1261
- act(() => {
1262
- ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
1263
- // Even in legacy mode, effects are deferred until after paint
1264
- expect(Scheduler).toFlushAndYieldThrough(['Count: (empty)']);
1265
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1266
- });
1297
+ it(
1298
+ 'in legacy mode, useEffect is deferred and updates finish synchronously ' +
1299
+ '(in a single batch)',
1300
+ () => {
1301
+ function Counter(props) {
1302
+ const [count, updateCount] = useState('(empty)');
1303
+ useEffect(() => {
1304
+ // Update multiple times. These should all be batched together in
1305
+ // a single render.
1306
+ updateCount(props.count);
1307
+ updateCount(props.count);
1308
+ updateCount(props.count);
1309
+ updateCount(props.count);
1310
+ updateCount(props.count);
1311
+ updateCount(props.count);
1312
+ }, [props.count]);
1313
+ return <Text text={'Count: ' + count} />;
1314
+ }
1315
+ act(() => {
1316
+ ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
1317
+ // Even in legacy mode, effects are deferred until after paint
1318
+ expect(Scheduler).toFlushAndYieldThrough(['Count: (empty)']);
1319
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1320
+ });
1321
1268
- // effects get fored on exiting act()
1269
- // There were multiple updates, but there should only be a
1270
- // single render
1271
- expect(Scheduler).toHaveYielded(['Count: 0']);
1272
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1273
- },
1274
- );
1275
-
1276
- it('flushSync is not allowed', () => {
1277
- function Counter(props) {
1278
- const [count, updateCount] = useState('(empty)');
1279
- useEffect(() => {
1280
- Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1281
- ReactNoop.flushSync(() => {
1282
- updateCount(props.count);
1283
- });
1284
- }, [props.count]);
1285
- return <Text text={'Count: ' + count} />;
1286
- }
1287
- act(() => {
1288
- ReactNoop.render(<Counter count={0} />, () =>
1289
- Scheduler.unstable_yieldValue('Sync effect'),
1322
+ // effects get fored on exiting act()
1323
+ // There were multiple updates, but there should only be a
1324
+ // single render
1325
+ expect(Scheduler).toHaveYielded(['Count: 0']);
1326
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1327
+ },
1328
);
1291
- expect(Scheduler).toFlushAndYieldThrough([
1292
- 'Count: (empty)',
1293
- 'Sync effect',
1294
- ]);
1295
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1296
- expect(() => {
1297
- ReactNoop.flushPassiveEffects();
1298
- }).toThrow('flushSync was called from inside a lifecycle method');
1299
- });
1300
- });
1329
1302
- it('unmounts previous effect', () => {
1303
- function Counter(props) {
1304
- useEffect(() => {
1305
- Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1306
- return () => {
1307
- Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1308
- };
1330
+ it('flushSync is not allowed', () => {
1331
+ function Counter(props) {
1332
+ const [count, updateCount] = useState('(empty)');
1333
+ useEffect(() => {
1334
+ Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1335
+ ReactNoop.flushSync(() => {
1336
+ updateCount(props.count);
1337
+ });
1338
+ }, [props.count]);
1339
+ return <Text text={'Count: ' + count} />;
1340
+ }
1341
+ act(() => {
1342
+ ReactNoop.render(<Counter count={0} />, () =>
1343
+ Scheduler.unstable_yieldValue('Sync effect'),
1344
+ );
1345
+ expect(Scheduler).toFlushAndYieldThrough([
1346
+ 'Count: (empty)',
1347
+ 'Sync effect',
1348
+ ]);
1349
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1350
+ expect(() => {
1351
+ ReactNoop.flushPassiveEffects();
1352
+ }).toThrow('flushSync was called from inside a lifecycle method');
1353
+ });
1354
});
1310
- return <Text text={'Count: ' + props.count} />;
1311
- }
1312
- act(() => {
1313
- ReactNoop.render(<Counter count={0} />, () =>
1314
- Scheduler.unstable_yieldValue('Sync effect'),
1315
- );
1316
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1317
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1318
- });
1355
1320
- expect(Scheduler).toHaveYielded(['Did create [0]']);
1356
+ it('unmounts previous effect', () => {
1357
+ function Counter(props) {
1358
+ useEffect(() => {
1359
+ Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1360
+ return () => {
1361
+ Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1362
+ };
1363
+ });
1364
+ return <Text text={'Count: ' + props.count} />;
1365
+ }
1366
+ act(() => {
1367
+ ReactNoop.render(<Counter count={0} />, () =>
1368
+ Scheduler.unstable_yieldValue('Sync effect'),
1369
+ );
1370
+ expect(Scheduler).toFlushAndYieldThrough([
1371
+ 'Count: 0',
1372
+ 'Sync effect',
1373
+ ]);
1374
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1375
+ });
1376
1322
- act(() => {
1323
- ReactNoop.render(<Counter count={1} />, () =>
1324
- Scheduler.unstable_yieldValue('Sync effect'),
1325
- );
1326
- expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1327
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1328
- });
1377
+ expect(Scheduler).toHaveYielded(['Did create [0]']);
1378
1330
- expect(Scheduler).toHaveYielded(['Did destroy [0]', 'Did create [1]']);
1331
- });
1379
+ act(() => {
1380
+ ReactNoop.render(<Counter count={1} />, () =>
1381
+ Scheduler.unstable_yieldValue('Sync effect'),
1382
+ );
1383
+ expect(Scheduler).toFlushAndYieldThrough([
1384
+ 'Count: 1',
1385
+ 'Sync effect',
1386
+ ]);
1387
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1388
+ });
1389
1333
- it('unmounts on deletion', () => {
1334
- function Counter(props) {
1335
- useEffect(() => {
1336
- Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1337
- return () => {
1338
- Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1339
- };
1390
+ expect(Scheduler).toHaveYielded([
1391
+ 'Did destroy [0]',
1392
+ 'Did create [1]',
1393
+ ]);
1394
});
1341
- return <Text text={'Count: ' + props.count} />;
1342
- }
1343
- act(() => {
1344
- ReactNoop.render(<Counter count={0} />, () =>
1345
- Scheduler.unstable_yieldValue('Sync effect'),
1346
- );
1347
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1348
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1349
- });
1395
1351
- expect(Scheduler).toHaveYielded(['Did create [0]']);
1396
+ it('unmounts on deletion', () => {
1397
+ function Counter(props) {
1398
+ useEffect(() => {
1399
+ Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1400
+ return () => {
1401
+ Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1402
+ };
1403
+ });
1404
+ return <Text text={'Count: ' + props.count} />;
1405
+ }
1406
+ act(() => {
1407
+ ReactNoop.render(<Counter count={0} />, () =>
1408
+ Scheduler.unstable_yieldValue('Sync effect'),
1409
+ );
1410
+ expect(Scheduler).toFlushAndYieldThrough([
1411
+ 'Count: 0',
1412
+ 'Sync effect',
1413
+ ]);
1414
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1415
+ });
1416
1353
- ReactNoop.render(null);
1354
- expect(Scheduler).toFlushAndYield(['Did destroy [0]']);
1355
- expect(ReactNoop.getChildren()).toEqual([]);
1356
- });
1417
+ expect(Scheduler).toHaveYielded(['Did create [0]']);
1418
1358
- it('unmounts on deletion after skipped effect', () => {
1359
- function Counter(props) {
1360
- useEffect(() => {
1361
- Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1362
- return () => {
1363
- Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1364
- };
1365
- }, []);
1366
- return <Text text={'Count: ' + props.count} />;
1367
- }
1368
- act(() => {
1369
- ReactNoop.render(<Counter count={0} />, () =>
1370
- Scheduler.unstable_yieldValue('Sync effect'),
1371
- );
1372
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1373
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1374
- });
1419
+ ReactNoop.render(null);
1420
+ expect(Scheduler).toFlushAndYield(['Did destroy [0]']);
1421
+ expect(ReactNoop.getChildren()).toEqual([]);
1422
+ });
1423
1376
- expect(Scheduler).toHaveYielded(['Did create [0]']);
1424
+ it('unmounts on deletion after skipped effect', () => {
1425
+ function Counter(props) {
1426
+ useEffect(() => {
1427
+ Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1428
+ return () => {
1429
+ Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1430
+ };
1431
+ }, []);
1432
+ return <Text text={'Count: ' + props.count} />;
1433
+ }
1434
+ act(() => {
1435
+ ReactNoop.render(<Counter count={0} />, () =>
1436
+ Scheduler.unstable_yieldValue('Sync effect'),
1437
+ );
1438
+ expect(Scheduler).toFlushAndYieldThrough([
1439
+ 'Count: 0',
1440
+ 'Sync effect',
1441
+ ]);
1442
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1443
+ });
1444
1378
- act(() => {
1379
- ReactNoop.render(<Counter count={1} />, () =>
1380
- Scheduler.unstable_yieldValue('Sync effect'),
1381
- );
1382
- expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1383
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1384
- });
1445
+ expect(Scheduler).toHaveYielded(['Did create [0]']);
1446
1386
- expect(Scheduler).toHaveYielded([]);
1447
+ act(() => {
1448
+ ReactNoop.render(<Counter count={1} />, () =>
1449
+ Scheduler.unstable_yieldValue('Sync effect'),
1450
+ );
1451
+ expect(Scheduler).toFlushAndYieldThrough([
1452
+ 'Count: 1',
1453
+ 'Sync effect',
1454
+ ]);
1455
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1456
+ });
1457
1388
- ReactNoop.render(null);
1389
- expect(Scheduler).toFlushAndYield(['Did destroy [0]']);
1390
- expect(ReactNoop.getChildren()).toEqual([]);
1391
- });
1458
+ expect(Scheduler).toHaveYielded([]);
1459
1393
- it('always fires effects if no dependencies are provided', () => {
1394
- function effect() {
1395
- Scheduler.unstable_yieldValue(`Did create`);
1396
- return () => {
1397
- Scheduler.unstable_yieldValue(`Did destroy`);
1398
- };
1399
- }
1400
- function Counter(props) {
1401
- useEffect(effect);
1402
- return <Text text={'Count: ' + props.count} />;
1403
- }
1404
- act(() => {
1405
- ReactNoop.render(<Counter count={0} />, () =>
1406
- Scheduler.unstable_yieldValue('Sync effect'),
1407
- );
1408
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1409
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1410
- });
1460
+ ReactNoop.render(null);
1461
+ expect(Scheduler).toFlushAndYield(['Did destroy [0]']);
1462
+ expect(ReactNoop.getChildren()).toEqual([]);
1463
+ });
1464
1412
- expect(Scheduler).toHaveYielded(['Did create']);
1465
+ it('always fires effects if no dependencies are provided', () => {
1466
+ function effect() {
1467
+ Scheduler.unstable_yieldValue(`Did create`);
1468
+ return () => {
1469
+ Scheduler.unstable_yieldValue(`Did destroy`);
1470
+ };
1471
+ }
1472
+ function Counter(props) {
1473
+ useEffect(effect);
1474
+ return <Text text={'Count: ' + props.count} />;
1475
+ }
1476
+ act(() => {
1477
+ ReactNoop.render(<Counter count={0} />, () =>
1478
+ Scheduler.unstable_yieldValue('Sync effect'),
1479
+ );
1480
+ expect(Scheduler).toFlushAndYieldThrough([
1481
+ 'Count: 0',
1482
+ 'Sync effect',
1483
+ ]);
1484
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1485
+ });
1486
1414
- act(() => {
1415
- ReactNoop.render(<Counter count={1} />, () =>
1416
- Scheduler.unstable_yieldValue('Sync effect'),
1417
- );
1418
- expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1419
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1420
- });
1487
+ expect(Scheduler).toHaveYielded(['Did create']);
1488
1422
- expect(Scheduler).toHaveYielded(['Did destroy', 'Did create']);
1489
+ act(() => {
1490
+ ReactNoop.render(<Counter count={1} />, () =>
1491
+ Scheduler.unstable_yieldValue('Sync effect'),
1492
+ );
1493
+ expect(Scheduler).toFlushAndYieldThrough([
1494
+ 'Count: 1',
1495
+ 'Sync effect',
1496
+ ]);
1497
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1498
+ });
1499
1424
- ReactNoop.render(null);
1425
- expect(Scheduler).toFlushAndYield(['Did destroy']);
1426
- expect(ReactNoop.getChildren()).toEqual([]);
1427
- });
1500
+ expect(Scheduler).toHaveYielded(['Did destroy', 'Did create']);
1501
1429
- it('skips effect if inputs have not changed', () => {
1430
- function Counter(props) {
1431
- const text = `${props.label}: ${props.count}`;
1432
- useEffect(() => {
1433
- Scheduler.unstable_yieldValue(`Did create [${text}]`);
1434
- return () => {
1435
- Scheduler.unstable_yieldValue(`Did destroy [${text}]`);
1436
- };
1437
- }, [props.label, props.count]);
1438
- return <Text text={text} />;
1439
- }
1440
- act(() => {
1441
- ReactNoop.render(<Counter label="Count" count={0} />, () =>
1442
- Scheduler.unstable_yieldValue('Sync effect'),
1443
- );
1444
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1445
- });
1502
+ ReactNoop.render(null);
1503
+ expect(Scheduler).toFlushAndYield(['Did destroy']);
1504
+ expect(ReactNoop.getChildren()).toEqual([]);
1505
+ });
1506
1447
- expect(Scheduler).toHaveYielded(['Did create [Count: 0]']);
1448
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1507
+ it('skips effect if inputs have not changed', () => {
1508
+ function Counter(props) {
1509
+ const text = `${props.label}: ${props.count}`;
1510
+ useEffect(() => {
1511
+ Scheduler.unstable_yieldValue(`Did create [${text}]`);
1512
+ return () => {
1513
+ Scheduler.unstable_yieldValue(`Did destroy [${text}]`);
1514
+ };
1515
+ }, [props.label, props.count]);
1516
+ return <Text text={text} />;
1517
+ }
1518
+ act(() => {
1519
+ ReactNoop.render(<Counter label="Count" count={0} />, () =>
1520
+ Scheduler.unstable_yieldValue('Sync effect'),
1521
+ );
1522
+ expect(Scheduler).toFlushAndYieldThrough([
1523
+ 'Count: 0',
1524
+ 'Sync effect',
1525
+ ]);
1526
+ });
1527
1450
- act(() => {
1451
- ReactNoop.render(<Counter label="Count" count={1} />, () =>
1452
- Scheduler.unstable_yieldValue('Sync effect'),
1453
- );
1454
- // Count changed
1455
- expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1456
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1457
- });
1528
+ expect(Scheduler).toHaveYielded(['Did create [Count: 0]']);
1529
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1530
1459
- expect(Scheduler).toHaveYielded([
1460
- 'Did destroy [Count: 0]',
1461
- 'Did create [Count: 1]',
1462
- ]);
1531
+ act(() => {
1532
+ ReactNoop.render(<Counter label="Count" count={1} />, () =>
1533
+ Scheduler.unstable_yieldValue('Sync effect'),
1534
+ );
1535
+ // Count changed
1536
+ expect(Scheduler).toFlushAndYieldThrough([
1537
+ 'Count: 1',
1538
+ 'Sync effect',
1539
+ ]);
1540
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1541
+ });
1542
1464
- act(() => {
1465
- ReactNoop.render(<Counter label="Count" count={1} />, () =>
1466
- Scheduler.unstable_yieldValue('Sync effect'),
1467
- );
1468
- // Nothing changed, so no effect should have fired
1469
- expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1470
- });
1543
+ expect(Scheduler).toHaveYielded([
1544
+ 'Did destroy [Count: 0]',
1545
+ 'Did create [Count: 1]',
1546
+ ]);
1547
1472
- expect(Scheduler).toHaveYielded([]);
1473
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1548
+ act(() => {
1549
+ ReactNoop.render(<Counter label="Count" count={1} />, () =>
1550
+ Scheduler.unstable_yieldValue('Sync effect'),
1551
+ );
1552
+ // Nothing changed, so no effect should have fired
1553
+ expect(Scheduler).toFlushAndYieldThrough([
1554
+ 'Count: 1',
1555
+ 'Sync effect',
1556
+ ]);
1557
+ });
1558
1475
- act(() => {
1476
- ReactNoop.render(<Counter label="Total" count={1} />, () =>
1477
- Scheduler.unstable_yieldValue('Sync effect'),
1478
- );
1479
- // Label changed
1480
- expect(Scheduler).toFlushAndYieldThrough(['Total: 1', 'Sync effect']);
1481
- expect(ReactNoop.getChildren()).toEqual([span('Total: 1')]);
1482
- });
1559
+ expect(Scheduler).toHaveYielded([]);
1560
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1561
1484
- expect(Scheduler).toHaveYielded([
1485
- 'Did destroy [Count: 1]',
1486
- 'Did create [Total: 1]',
1487
- ]);
1488
- });
1562
+ act(() => {
1563
+ ReactNoop.render(<Counter label="Total" count={1} />, () =>
1564
+ Scheduler.unstable_yieldValue('Sync effect'),
1565
+ );
1566
+ // Label changed
1567
+ expect(Scheduler).toFlushAndYieldThrough([
1568
+ 'Total: 1',
1569
+ 'Sync effect',
1570
+ ]);
1571
+ expect(ReactNoop.getChildren()).toEqual([span('Total: 1')]);
1572
+ });
1573
1490
- it('multiple effects', () => {
1491
- function Counter(props) {
1492
- useEffect(() => {
1493
- Scheduler.unstable_yieldValue(`Did commit 1 [${props.count}]`);
1494
- });
1495
- useEffect(() => {
1496
- Scheduler.unstable_yieldValue(`Did commit 2 [${props.count}]`);
1574
+ expect(Scheduler).toHaveYielded([
1575
+ 'Did destroy [Count: 1]',
1576
+ 'Did create [Total: 1]',
1577
+ ]);
1578
});
1498
- return <Text text={'Count: ' + props.count} />;
1499
- }
1500
- act(() => {
1501
- ReactNoop.render(<Counter count={0} />, () =>
1502
- Scheduler.unstable_yieldValue('Sync effect'),
1503
- );
1504
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1505
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1506
- });
1579
1508
- expect(Scheduler).toHaveYielded(['Did commit 1 [0]', 'Did commit 2 [0]']);
1580
+ it('multiple effects', () => {
1581
+ function Counter(props) {
1582
+ useEffect(() => {
1583
+ Scheduler.unstable_yieldValue(`Did commit 1 [${props.count}]`);
1584
+ });
1585
+ useEffect(() => {
1586
+ Scheduler.unstable_yieldValue(`Did commit 2 [${props.count}]`);
1587
+ });
1588
+ return <Text text={'Count: ' + props.count} />;
1589
+ }
1590
+ act(() => {
1591
+ ReactNoop.render(<Counter count={0} />, () =>
1592
+ Scheduler.unstable_yieldValue('Sync effect'),
1593
+ );
1594
+ expect(Scheduler).toFlushAndYieldThrough([
1595
+ 'Count: 0',
1596
+ 'Sync effect',
1597
+ ]);
1598
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1599
+ });
1600
1510
- act(() => {
1511
- ReactNoop.render(<Counter count={1} />, () =>
1512
- Scheduler.unstable_yieldValue('Sync effect'),
1513
- );
1514
- expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1515
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1516
- });
1517
- expect(Scheduler).toHaveYielded(['Did commit 1 [1]', 'Did commit 2 [1]']);
1518
- });
1601
+ expect(Scheduler).toHaveYielded([
1602
+ 'Did commit 1 [0]',
1603
+ 'Did commit 2 [0]',
1604
+ ]);
1605
1520
- it('unmounts all previous effects before creating any new ones', () => {
1521
- function Counter(props) {
1522
- useEffect(() => {
1523
- Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1524
- return () => {
1525
- Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1526
- };
1527
- });
1528
- useEffect(() => {
1529
- Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1530
- return () => {
1531
- Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1532
- };
1606
+ act(() => {
1607
+ ReactNoop.render(<Counter count={1} />, () =>
1608
+ Scheduler.unstable_yieldValue('Sync effect'),
1609
+ );
1610
+ expect(Scheduler).toFlushAndYieldThrough([
1611
+ 'Count: 1',
1612
+ 'Sync effect',
1613
+ ]);
1614
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1615
+ });
1616
+ expect(Scheduler).toHaveYielded([
1617
+ 'Did commit 1 [1]',
1618
+ 'Did commit 2 [1]',
1619
+ ]);
1620
});
1534
- return <Text text={'Count: ' + props.count} />;
1535
- }
1536
- act(() => {
1537
- ReactNoop.render(<Counter count={0} />, () =>
1538
- Scheduler.unstable_yieldValue('Sync effect'),
1539
- );
1540
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1541
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1542
- });
1621
1544
- expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1622
+ it('unmounts all previous effects before creating any new ones', () => {
1623
+ function Counter(props) {
1624
+ useEffect(() => {
1625
+ Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1626
+ return () => {
1627
+ Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1628
+ };
1629
+ });
1630
+ useEffect(() => {
1631
+ Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1632
+ return () => {
1633
+ Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1634
+ };
1635
+ });
1636
+ return <Text text={'Count: ' + props.count} />;
1637
+ }
1638
+ act(() => {
1639
+ ReactNoop.render(<Counter count={0} />, () =>
1640
+ Scheduler.unstable_yieldValue('Sync effect'),
1641
+ );
1642
+ expect(Scheduler).toFlushAndYieldThrough([
1643
+ 'Count: 0',
1644
+ 'Sync effect',
1645
+ ]);
1646
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1647
+ });
1648
1546
- act(() => {
1547
- ReactNoop.render(<Counter count={1} />, () =>
1548
- Scheduler.unstable_yieldValue('Sync effect'),
1549
- );
1550
- expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1551
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1552
- });
1553
- expect(Scheduler).toHaveYielded([
1554
- 'Unmount A [0]',
1555
- 'Unmount B [0]',
1556
- 'Mount A [1]',
1557
- 'Mount B [1]',
1558
- ]);
1559
- });
1649
+ expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1650
1561
- it('unmounts all previous effects between siblings before creating any new ones', () => {
1562
- function Counter({count, label}) {
1563
- useEffect(() => {
1564
- Scheduler.unstable_yieldValue(`Mount ${label} [${count}]`);
1565
- return () => {
1566
- Scheduler.unstable_yieldValue(`Unmount ${label} [${count}]`);
1567
- };
1651
+ act(() => {
1652
+ ReactNoop.render(<Counter count={1} />, () =>
1653
+ Scheduler.unstable_yieldValue('Sync effect'),
1654
+ );
1655
+ expect(Scheduler).toFlushAndYieldThrough([
1656
+ 'Count: 1',
1657
+ 'Sync effect',
1658
+ ]);
1659
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1660
+ });
1661
+ expect(Scheduler).toHaveYielded([
1662
+ 'Unmount A [0]',
1663
+ 'Unmount B [0]',
1664
+ 'Mount A [1]',
1665
+ 'Mount B [1]',
1666
+ ]);
1667
});
1569
- return <Text text={`${label} ${count}`} />;
1570
- }
1571
- act(() => {
1572
- ReactNoop.render(
1573
- <React.Fragment>
1574
- <Counter label="A" count={0} />
1575
- <Counter label="B" count={0} />
1576
- </React.Fragment>,
1577
- () => Scheduler.unstable_yieldValue('Sync effect'),
1578
- );
1579
- expect(Scheduler).toFlushAndYieldThrough(['A 0', 'B 0', 'Sync effect']);
1580
- expect(ReactNoop.getChildren()).toEqual([span('A 0'), span('B 0')]);
1581
- });
1668
1583
- expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1669
+ if (runAllPassiveEffectDestroysBeforeCreates) {
1670
+ it('unmounts all previous effects between siblings before creating any new ones', () => {
1671
+ function Counter({count, label}) {
1672
+ useEffect(() => {
1673
+ Scheduler.unstable_yieldValue(`Mount ${label} [${count}]`);
1674
+ return () => {
1675
+ Scheduler.unstable_yieldValue(`Unmount ${label} [${count}]`);
1676
+ };
1677
+ });
1678
+ return <Text text={`${label} ${count}`} />;
1679
+ }
1680
+ act(() => {
1681
+ ReactNoop.render(
1682
+ <React.Fragment>
1683
+ <Counter label="A" count={0} />
1684
+ <Counter label="B" count={0} />
1685
+ </React.Fragment>,
1686
+ () => Scheduler.unstable_yieldValue('Sync effect'),
1687
+ );
1688
+ expect(Scheduler).toFlushAndYieldThrough([
1689
+ 'A 0',
1690
+ 'B 0',
1691
+ 'Sync effect',
1692
+ ]);
1693
+ expect(ReactNoop.getChildren()).toEqual([
1694
+ span('A 0'),
1695
+ span('B 0'),
1696
+ ]);
1697
+ });
1698
1585
- act(() => {
1586
- ReactNoop.render(
1587
- <React.Fragment>
1588
- <Counter label="A" count={1} />
1589
- <Counter label="B" count={1} />
1590
- </React.Fragment>,
1591
- () => Scheduler.unstable_yieldValue('Sync effect'),
1592
- );
1593
- expect(Scheduler).toFlushAndYieldThrough(['A 1', 'B 1', 'Sync effect']);
1594
- expect(ReactNoop.getChildren()).toEqual([span('A 1'), span('B 1')]);
1595
- });
1596
- expect(Scheduler).toHaveYielded([
1597
- 'Unmount A [0]',
1598
- 'Unmount B [0]',
1599
- 'Mount A [1]',
1600
- 'Mount B [1]',
1601
- ]);
1602
-
1603
- act(() => {
1604
- ReactNoop.render(
1605
- <React.Fragment>
1606
- <Counter label="B" count={2} />
1607
- <Counter label="C" count={0} />
1608
- </React.Fragment>,
1609
- () => Scheduler.unstable_yieldValue('Sync effect'),
1610
- );
1611
- expect(Scheduler).toFlushAndYieldThrough(['B 2', 'C 0', 'Sync effect']);
1612
- expect(ReactNoop.getChildren()).toEqual([span('B 2'), span('C 0')]);
1613
- });
1614
- expect(Scheduler).toHaveYielded([
1615
- 'Unmount A [1]',
1616
- 'Unmount B [1]',
1617
- 'Mount B [2]',
1618
- 'Mount C [0]',
1619
- ]);
1620
- });
1699
+ expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1700
+
1701
+ act(() => {
1702
+ ReactNoop.render(
1703
+ <React.Fragment>
1704
+ <Counter label="A" count={1} />
1705
+ <Counter label="B" count={1} />
1706
+ </React.Fragment>,
1707
+ () => Scheduler.unstable_yieldValue('Sync effect'),
1708
+ );
1709
+ expect(Scheduler).toFlushAndYieldThrough([
1710
+ 'A 1',
1711
+ 'B 1',
1712
+ 'Sync effect',
1713
+ ]);
1714
+ expect(ReactNoop.getChildren()).toEqual([
1715
+ span('A 1'),
1716
+ span('B 1'),
1717
+ ]);
1718
+ });
1719
+ expect(Scheduler).toHaveYielded([
1720
+ 'Unmount A [0]',
1721
+ 'Unmount B [0]',
1722
+ 'Mount A [1]',
1723
+ 'Mount B [1]',
1724
+ ]);
1725
+
1726
+ act(() => {
1727
+ ReactNoop.render(
1728
+ <React.Fragment>
1729
+ <Counter label="B" count={2} />
1730
+ <Counter label="C" count={0} />
1731
+ </React.Fragment>,
1732
+ () => Scheduler.unstable_yieldValue('Sync effect'),
1733
+ );
1734
+ expect(Scheduler).toFlushAndYieldThrough([
1735
+ 'B 2',
1736
+ 'C 0',
1737
+ 'Sync effect',
1738
+ ]);
1739
+ expect(ReactNoop.getChildren()).toEqual([
1740
+ span('B 2'),
1741
+ span('C 0'),
1742
+ ]);
1743
+ });
1744
+ expect(Scheduler).toHaveYielded([
1745
+ 'Unmount A [1]',
1746
+ 'Unmount B [1]',
1747
+ 'Mount B [2]',
1748
+ 'Mount C [0]',
1749
+ ]);
1750
+ });
1751
+ }
1752
1622
- it('handles errors on mount', () => {
1623
- function Counter(props) {
1624
- useEffect(() => {
1625
- Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1626
- return () => {
1627
- Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1628
- };
1753
+ it('handles errors in create on mount', () => {
1754
+ function Counter(props) {
1755
+ useEffect(() => {
1756
+ Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1757
+ return () => {
1758
+ Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1759
+ };
1760
+ });
1761
+ useEffect(() => {
1762
+ Scheduler.unstable_yieldValue('Oops!');
1763
+ throw new Error('Oops!');
1764
+ // eslint-disable-next-line no-unreachable
1765
+ Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1766
+ return () => {
1767
+ Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1768
+ };
1769
+ });
1770
+ return <Text text={'Count: ' + props.count} />;
1771
+ }
1772
+ act(() => {
1773
+ ReactNoop.render(<Counter count={0} />, () =>
1774
+ Scheduler.unstable_yieldValue('Sync effect'),
1775
+ );
1776
+ expect(Scheduler).toFlushAndYieldThrough([
1777
+ 'Count: 0',
1778
+ 'Sync effect',
1779
+ ]);
1780
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1781
+ expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1782
+ });
1783
+
1784
+ expect(Scheduler).toHaveYielded([
1785
+ 'Mount A [0]',
1786
+ 'Oops!',
1787
+ // Clean up effect A. There's no effect B to clean-up, because it
1788
+ // never mounted.
1789
+ 'Unmount A [0]',
1790
+ ]);
1791
+ expect(ReactNoop.getChildren()).toEqual([]);
1792
});
1630
- useEffect(() => {
1631
- Scheduler.unstable_yieldValue('Oops!');
1632
- throw new Error('Oops!');
1633
- // eslint-disable-next-line no-unreachable
1634
- Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1635
- return () => {
1636
- Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1637
- };
1793
+
1794
+ it('handles errors in create on update', () => {
1795
+ function Counter(props) {
1796
+ useEffect(() => {
1797
+ Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1798
+ return () => {
1799
+ Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1800
+ };
1801
+ });
1802
+ useEffect(() => {
1803
+ if (props.count === 1) {
1804
+ Scheduler.unstable_yieldValue('Oops!');
1805
+ throw new Error('Oops!');
1806
+ }
1807
+ Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1808
+ return () => {
1809
+ Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1810
+ };
1811
+ });
1812
+ return <Text text={'Count: ' + props.count} />;
1813
+ }
1814
+ act(() => {
1815
+ ReactNoop.render(<Counter count={0} />, () =>
1816
+ Scheduler.unstable_yieldValue('Sync effect'),
1817
+ );
1818
+ expect(Scheduler).toFlushAndYieldThrough([
1819
+ 'Count: 0',
1820
+ 'Sync effect',
1821
+ ]);
1822
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1823
+ ReactNoop.flushPassiveEffects();
1824
+ expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1825
+ });
1826
+
1827
+ act(() => {
1828
+ // This update will trigger an error
1829
+ ReactNoop.render(<Counter count={1} />, () =>
1830
+ Scheduler.unstable_yieldValue('Sync effect'),
1831
+ );
1832
+ expect(Scheduler).toFlushAndYieldThrough([
1833
+ 'Count: 1',
1834
+ 'Sync effect',
1835
+ ]);
1836
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1837
+ expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1838
+ expect(Scheduler).toHaveYielded(
1839
+ deferPassiveEffectCleanupDuringUnmount &&
1840
+ runAllPassiveEffectDestroysBeforeCreates
1841
+ ? ['Unmount A [0]', 'Unmount B [0]', 'Mount A [1]', 'Oops!']
1842
+ : [
1843
+ 'Unmount A [0]',
1844
+ 'Unmount B [0]',
1845
+ 'Mount A [1]',
1846
+ 'Oops!',
1847
+ 'Unmount A [1]',
1848
+ ],
1849
+ );
1850
+ expect(ReactNoop.getChildren()).toEqual([]);
1851
+ });
1852
+ if (
1853
+ deferPassiveEffectCleanupDuringUnmount &&
1854
+ runAllPassiveEffectDestroysBeforeCreates
1855
+ ) {
1856
+ expect(Scheduler).toHaveYielded([
1857
+ // Clean up effect A runs passively on unmount.
1858
+ // There's no effect B to clean-up, because it never mounted.
1859
+ 'Unmount A [1]',
1860
+ ]);
1861
+ }
1862
});
1639
- return <Text text={'Count: ' + props.count} />;
1640
- }
1641
- act(() => {
1642
- ReactNoop.render(<Counter count={0} />, () =>
1643
- Scheduler.unstable_yieldValue('Sync effect'),
1644
- );
1645
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1646
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1647
- expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1648
- });
1863
1650
- expect(Scheduler).toHaveYielded([
1651
- 'Mount A [0]',
1652
- 'Oops!',
1653
- // Clean up effect A. There's no effect B to clean-up, because it
1654
- // never mounted.
1655
- 'Unmount A [0]',
1656
- ]);
1657
- expect(ReactNoop.getChildren()).toEqual([]);
1658
- });
1864
+ it('handles errors in destroy on update', () => {
1865
+ function Counter(props) {
1866
+ useEffect(() => {
1867
+ Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1868
+ return () => {
1869
+ Scheduler.unstable_yieldValue('Oops!');
1870
+ if (props.count === 0) {
1871
+ throw new Error('Oops!');
1872
+ }
1873
+ };
1874
+ });
1875
+ useEffect(() => {
1876
+ Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1877
+ return () => {
1878
+ Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1879
+ };
1880
+ });
1881
+ return <Text text={'Count: ' + props.count} />;
1882
+ }
1883
1660
- it('handles errors on update', () => {
1661
- function Counter(props) {
1662
- useEffect(() => {
1663
- Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1664
- return () => {
1665
- Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1666
- };
1884
+ act(() => {
1885
+ ReactNoop.render(<Counter count={0} />, () =>
1886
+ Scheduler.unstable_yieldValue('Sync effect'),
1887
+ );
1888
+ expect(Scheduler).toFlushAndYieldThrough([
1889
+ 'Count: 0',
1890
+ 'Sync effect',
1891
+ ]);
1892
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1893
+ ReactNoop.flushPassiveEffects();
1894
+ expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1895
+ });
1896
+
1897
+ if (
1898
+ deferPassiveEffectCleanupDuringUnmount &&
1899
+ runAllPassiveEffectDestroysBeforeCreates
1900
+ ) {
1901
+ act(() => {
1902
+ // This update will trigger an error during passive effect unmount
1903
+ ReactNoop.render(<Counter count={1} />, () =>
1904
+ Scheduler.unstable_yieldValue('Sync effect'),
1905
+ );
1906
+ expect(Scheduler).toFlushAndYieldThrough([
1907
+ 'Count: 1',
1908
+ 'Sync effect',
1909
+ ]);
1910
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1911
+ expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1912
+
1913
+ // This branch enables a feature flag that flushes all passive destroys in a
1914
+ // separate pass before flushing any passive creates.
1915
+ // A result of this two-pass flush is that an error thrown from unmount does
1916
+ // not block the subsequent create functions from being run.
1917
+ expect(Scheduler).toHaveYielded([
1918
+ 'Oops!',
1919
+ 'Unmount B [0]',
1920
+ 'Mount A [1]',
1921
+ 'Mount B [1]',
1922
+ ]);
1923
+ });
1924
+
1925
+ // <Counter> gets unmounted because an error is thrown above.
1926
+ // The remaining destroy functions are run later on unmount, since they're passive.
1927
+ // In this case, one of them throws again (because of how the test is written).
1928
+ expect(Scheduler).toHaveYielded(['Oops!', 'Unmount B [1]']);
1929
+ expect(ReactNoop.getChildren()).toEqual([]);
1930
+ } else {
1931
+ act(() => {
1932
+ // This update will trigger an error during passive effect unmount
1933
+ ReactNoop.render(<Counter count={1} />, () =>
1934
+ Scheduler.unstable_yieldValue('Sync effect'),
1935
+ );
1936
+ expect(() => {
1937
+ expect(Scheduler).toFlushAndYield(['Count: 1', 'Sync effect']);
1938
+ }).toThrow('Oops!');
1939
+ expect(ReactNoop.getChildren()).toEqual([]);
1940
+ ReactNoop.flushPassiveEffects();
1941
+ });
1942
+ }
1943
});
1668
- useEffect(() => {
1669
- if (props.count === 1) {
1670
- Scheduler.unstable_yieldValue('Oops!');
1671
- throw new Error('Oops!');
1672
- }
1673
- Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1674
- return () => {
1675
- Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1676
- };
1944
+
1945
+ it('works with memo', () => {
1946
+ function Counter({count}) {
1947
+ useLayoutEffect(() => {
1948
+ Scheduler.unstable_yieldValue('Mount: ' + count);
1949
+ return () => Scheduler.unstable_yieldValue('Unmount: ' + count);
1950
+ });
1951
+ return <Text text={'Count: ' + count} />;
1952
+ }
1953
+ Counter = memo(Counter);
1954
+
1955
+ ReactNoop.render(<Counter count={0} />, () =>
1956
+ Scheduler.unstable_yieldValue('Sync effect'),
1957
+ );
1958
+ expect(Scheduler).toFlushAndYieldThrough([
1959
+ 'Count: 0',
1960
+ 'Mount: 0',
1961
+ 'Sync effect',
1962
+ ]);
1963
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1964
+
1965
+ ReactNoop.render(<Counter count={1} />, () =>
1966
+ Scheduler.unstable_yieldValue('Sync effect'),
1967
+ );
1968
+ expect(Scheduler).toFlushAndYieldThrough([
1969
+ 'Count: 1',
1970
+ 'Unmount: 0',
1971
+ 'Mount: 1',
1972
+ 'Sync effect',
1973
+ ]);
1974
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1975
+
1976
+ ReactNoop.render(null);
1977
+ expect(Scheduler).toFlushAndYieldThrough(['Unmount: 1']);
1978
+ expect(ReactNoop.getChildren()).toEqual([]);
1979
});
1678
- return <Text text={'Count: ' + props.count} />;
1679
- }
1680
- act(() => {
1681
- ReactNoop.render(<Counter count={0} />, () =>
1682
- Scheduler.unstable_yieldValue('Sync effect'),
1683
- );
1684
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1685
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1686
- ReactNoop.flushPassiveEffects();
1687
- expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1980
});
1981
1690
- act(() => {
1691
- // This update will trigger an error
1692
- ReactNoop.render(<Counter count={1} />, () =>
1693
- Scheduler.unstable_yieldValue('Sync effect'),
1694
- );
1695
- expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1696
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1697
- expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1698
- expect(Scheduler).toHaveYielded([
1699
- 'Unmount A [0]',
1700
- 'Unmount B [0]',
1701
- 'Mount A [1]',
1702
- 'Oops!',
1703
- ]);
1704
- expect(ReactNoop.getChildren()).toEqual([]);
1705
- });
1706
- expect(Scheduler).toHaveYielded([
1707
- // Clean up effect A runs passively on unmount.
1708
- // There's no effect B to clean-up, because it never mounted.
1709
- 'Unmount A [1]',
1710
- ]);
1711
- });
1982
+ describe('useLayoutEffect', () => {
1983
+ it('fires layout effects after the host has been mutated', () => {
1984
+ function getCommittedText() {
1985
+ const yields = Scheduler.unstable_clearYields();
1986
+ const children = ReactNoop.getChildren();
1987
+ Scheduler.unstable_yieldValue(yields);
1988
+ if (children === null) {
1989
+ return null;
1990
+ }
1991
+ return children[0].prop;
1992
+ }
1993
1713
- it('handles errors on unmount', () => {
1714
- function Counter(props) {
1715
- useEffect(() => {
1716
- Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1717
- return () => {
1718
- Scheduler.unstable_yieldValue('Oops!');
1719
- throw new Error('Oops!');
1720
- };
1721
- });
1722
- useEffect(() => {
1723
- Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1724
- return () => {
1725
- Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1726
- };
1727
- });
1728
- return <Text text={'Count: ' + props.count} />;
1729
- }
1994
+ function Counter(props) {
1995
+ useLayoutEffect(() => {
1996
+ Scheduler.unstable_yieldValue(`Current: ${getCommittedText()}`);
1997
+ });
1998
+ return <Text text={props.count} />;
1999
+ }
2000
1731
- act(() => {
1732
- ReactNoop.render(<Counter count={0} />, () =>
1733
- Scheduler.unstable_yieldValue('Sync effect'),
1734
- );
1735
- expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1736
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1737
- ReactNoop.flushPassiveEffects();
1738
- expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1739
- });
2001
+ ReactNoop.render(<Counter count={0} />, () =>
2002
+ Scheduler.unstable_yieldValue('Sync effect'),
2003
+ );
2004
+ expect(Scheduler).toFlushAndYieldThrough([
2005
+ [0],
2006
+ 'Current: 0',
2007
+ 'Sync effect',
2008
+ ]);
2009
+ expect(ReactNoop.getChildren()).toEqual([span(0)]);
2010
1741
- act(() => {
1742
- // This update will trigger an error during passive effect unmount
1743
- ReactNoop.render(<Counter count={1} />, () =>
1744
- Scheduler.unstable_yieldValue('Sync effect'),
1745
- );
1746
- expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1747
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1748
- expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1749
-
1750
- // This tests enables a feature flag that flushes all passive destroys in a
1751
- // separate pass before flushing any passive creates.
1752
- // A result of this two-pass flush is that an error thrown from unmount does
1753
- // not block the subsequent create functions from being run.
1754
- expect(Scheduler).toHaveYielded([
1755
- 'Oops!',
1756
- 'Unmount B [0]',
1757
- 'Mount A [1]',
1758
- 'Mount B [1]',
1759
- ]);
1760
- });
2011
+ ReactNoop.render(<Counter count={1} />, () =>
2012
+ Scheduler.unstable_yieldValue('Sync effect'),
2013
+ );
2014
+ expect(Scheduler).toFlushAndYieldThrough([
2015
+ [1],
2016
+ 'Current: 1',
2017
+ 'Sync effect',
2018
+ ]);
2019
+ expect(ReactNoop.getChildren()).toEqual([span(1)]);
2020
+ });
2021
1762
- // <Counter> gets unmounted because an error is thrown above.
1763
- // The remaining destroy functions are run later on unmount, since they're passive.
1764
- // In this case, one of them throws again (because of how the test is written).
1765
- expect(Scheduler).toHaveYielded(['Oops!', 'Unmount B [1]']);
1766
- expect(ReactNoop.getChildren()).toEqual([]);
1767
- });
2022
+ it('force flushes passive effects before firing new layout effects', () => {
2023
+ let committedText = '(empty)';
2024
+
2025
+ function Counter(props) {
2026
+ useLayoutEffect(() => {
2027
+ // Normally this would go in a mutation effect, but this test
2028
+ // intentionally omits a mutation effect.
2029
+ committedText = props.count + '';
2030
+
2031
+ Scheduler.unstable_yieldValue(
2032
+ `Mount layout [current: ${committedText}]`,
2033
+ );
2034
+ return () => {
2035
+ Scheduler.unstable_yieldValue(
2036
+ `Unmount layout [current: ${committedText}]`,
2037
+ );
2038
+ };
2039
+ });
2040
+ useEffect(() => {
2041
+ Scheduler.unstable_yieldValue(
2042
+ `Mount normal [current: ${committedText}]`,
2043
+ );
2044
+ return () => {
2045
+ Scheduler.unstable_yieldValue(
2046
+ `Unmount normal [current: ${committedText}]`,
2047
+ );
2048
+ };
2049
+ });
2050
+ return null;
2051
+ }
2052
+
2053
+ act(() => {
2054
+ ReactNoop.render(<Counter count={0} />, () =>
2055
+ Scheduler.unstable_yieldValue('Sync effect'),
2056
+ );
2057
+ expect(Scheduler).toFlushAndYieldThrough([
2058
+ 'Mount layout [current: 0]',
2059
+ 'Sync effect',
2060
+ ]);
2061
+ expect(committedText).toEqual('0');
2062
+ ReactNoop.render(<Counter count={1} />, () =>
2063
+ Scheduler.unstable_yieldValue('Sync effect'),
2064
+ );
2065
+ expect(Scheduler).toFlushAndYieldThrough([
2066
+ 'Mount normal [current: 0]',
2067
+ 'Unmount layout [current: 0]',
2068
+ 'Mount layout [current: 1]',
2069
+ 'Sync effect',
2070
+ ]);
2071
+ expect(committedText).toEqual('1');
2072
+ });
2073
1769
- it('works with memo', () => {
1770
- function Counter({count}) {
1771
- useLayoutEffect(() => {
1772
- Scheduler.unstable_yieldValue('Mount: ' + count);
1773
- return () => Scheduler.unstable_yieldValue('Unmount: ' + count);
2074
+ expect(Scheduler).toHaveYielded([
2075
+ 'Unmount normal [current: 1]',
2076
+ 'Mount normal [current: 1]',
2077
+ ]);
2078
});
1775
- return <Text text={'Count: ' + count} />;
1776
- }
1777
- Counter = memo(Counter);
1778
-
1779
- ReactNoop.render(<Counter count={0} />, () =>
1780
- Scheduler.unstable_yieldValue('Sync effect'),
1781
- );
1782
- expect(Scheduler).toFlushAndYieldThrough([
1783
- 'Count: 0',
1784
- 'Mount: 0',
1785
- 'Sync effect',
1786
- ]);
1787
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1788
-
1789
- ReactNoop.render(<Counter count={1} />, () =>
1790
- Scheduler.unstable_yieldValue('Sync effect'),
1791
- );
1792
- expect(Scheduler).toFlushAndYieldThrough([
1793
- 'Count: 1',
1794
- 'Unmount: 0',
1795
- 'Mount: 1',
1796
- 'Sync effect',
1797
- ]);
1798
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1799
-
1800
- ReactNoop.render(null);
1801
- expect(Scheduler).toFlushAndYieldThrough(['Unmount: 1']);
1802
- expect(ReactNoop.getChildren()).toEqual([]);
1803
- });
1804
- });
2079
+ });
2080
1806
- describe('useLayoutEffect', () => {
1807
- it('fires layout effects after the host has been mutated', () => {
1808
- function getCommittedText() {
1809
- const yields = Scheduler.unstable_clearYields();
1810
- const children = ReactNoop.getChildren();
1811
- Scheduler.unstable_yieldValue(yields);
1812
- if (children === null) {
1813
- return null;
1814
- }
1815
- return children[0].prop;
1816
- }
2081
+ describe('useCallback', () => {
2082
+ it('memoizes callback by comparing inputs', () => {
2083
+ class IncrementButton extends React.PureComponent {
2084
+ increment = () => {
2085
+ this.props.increment();
2086
+ };
2087
+ render() {
2088
+ return <Text text="Increment" />;
2089
+ }
2090
+ }
2091
1818
- function Counter(props) {
1819
- useLayoutEffect(() => {
1820
- Scheduler.unstable_yieldValue(`Current: ${getCommittedText()}`);
1821
- });
1822
- return <Text text={props.count} />;
1823
- }
2092
+ function Counter({incrementBy}) {
2093
+ const [count, updateCount] = useState(0);
2094
+ const increment = useCallback(
2095
+ () => updateCount(c => c + incrementBy),
2096
+ [incrementBy],
2097
+ );
2098
+ return (
2099
+ <>
2100
+ <IncrementButton increment={increment} ref={button} />
2101
+ <Text text={'Count: ' + count} />
2102
+ </>
2103
+ );
2104
+ }
2105
1825
- ReactNoop.render(<Counter count={0} />, () =>
1826
- Scheduler.unstable_yieldValue('Sync effect'),
1827
- );
1828
- expect(Scheduler).toFlushAndYieldThrough([
1829
- [0],
1830
- 'Current: 0',
1831
- 'Sync effect',
1832
- ]);
1833
- expect(ReactNoop.getChildren()).toEqual([span(0)]);
1834
-
1835
- ReactNoop.render(<Counter count={1} />, () =>
1836
- Scheduler.unstable_yieldValue('Sync effect'),
1837
- );
1838
- expect(Scheduler).toFlushAndYieldThrough([
1839
- [1],
1840
- 'Current: 1',
1841
- 'Sync effect',
1842
- ]);
1843
- expect(ReactNoop.getChildren()).toEqual([span(1)]);
1844
- });
2106
+ const button = React.createRef(null);
2107
+ ReactNoop.render(<Counter incrementBy={1} />);
2108
+ expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']);
2109
+ expect(ReactNoop.getChildren()).toEqual([
2110
+ span('Increment'),
2111
+ span('Count: 0'),
2112
+ ]);
2113
1846
- it('force flushes passive effects before firing new layout effects', () => {
1847
- let committedText = '(empty)';
2114
+ act(button.current.increment);
2115
+ expect(Scheduler).toHaveYielded([
2116
+ // Button should not re-render, because its props haven't changed
2117
+ // 'Increment',
2118
+ 'Count: 1',
2119
+ ]);
2120
+ expect(ReactNoop.getChildren()).toEqual([
2121
+ span('Increment'),
2122
+ span('Count: 1'),
2123
+ ]);
2124
1849
- function Counter(props) {
1850
- useLayoutEffect(() => {
1851
- // Normally this would go in a mutation effect, but this test
1852
- // intentionally omits a mutation effect.
1853
- committedText = props.count + '';
2125
+ // Increase the increment amount
2126
+ ReactNoop.render(<Counter incrementBy={10} />);
2127
+ expect(Scheduler).toFlushAndYield([
2128
+ // Inputs did change this time
2129
+ 'Increment',
2130
+ 'Count: 1',
2131
+ ]);
2132
+ expect(ReactNoop.getChildren()).toEqual([
2133
+ span('Increment'),
2134
+ span('Count: 1'),
2135
+ ]);
2136
1855
- Scheduler.unstable_yieldValue(
1856
- `Mount layout [current: ${committedText}]`,
1857
- );
1858
- return () => {
1859
- Scheduler.unstable_yieldValue(
1860
- `Unmount layout [current: ${committedText}]`,
1861
- );
1862
- };
1863
- });
1864
- useEffect(() => {
1865
- Scheduler.unstable_yieldValue(
1866
- `Mount normal [current: ${committedText}]`,
1867
- );
1868
- return () => {
1869
- Scheduler.unstable_yieldValue(
1870
- `Unmount normal [current: ${committedText}]`,
1871
- );
1872
- };
2137
+ // Callback should have updated
2138
+ act(button.current.increment);
2139
+ expect(Scheduler).toHaveYielded(['Count: 11']);
2140
+ expect(ReactNoop.getChildren()).toEqual([
2141
+ span('Increment'),
2142
+ span('Count: 11'),
2143
+ ]);
2144
});
1874
- return null;
1875
- }
1876
-
1877
- act(() => {
1878
- ReactNoop.render(<Counter count={0} />, () =>
1879
- Scheduler.unstable_yieldValue('Sync effect'),
1880
- );
1881
- expect(Scheduler).toFlushAndYieldThrough([
1882
- 'Mount layout [current: 0]',
1883
- 'Sync effect',
1884
- ]);
1885
- expect(committedText).toEqual('0');
1886
- ReactNoop.render(<Counter count={1} />, () =>
1887
- Scheduler.unstable_yieldValue('Sync effect'),
1888
- );
1889
- expect(Scheduler).toFlushAndYieldThrough([
1890
- 'Mount normal [current: 0]',
1891
- 'Unmount layout [current: 0]',
1892
- 'Mount layout [current: 1]',
1893
- 'Sync effect',
1894
- ]);
1895
- expect(committedText).toEqual('1');
2145
});
2146
1898
- expect(Scheduler).toHaveYielded([
1899
- 'Unmount normal [current: 1]',
1900
- 'Mount normal [current: 1]',
1901
- ]);
1902
- });
1903
- });
2147
+ describe('useMemo', () => {
2148
+ it('memoizes value by comparing to previous inputs', () => {
2149
+ function CapitalizedText(props) {
2150
+ const text = props.text;
2151
+ const capitalizedText = useMemo(() => {
2152
+ Scheduler.unstable_yieldValue(`Capitalize '${text}'`);
2153
+ return text.toUpperCase();
2154
+ }, [text]);
2155
+ return <Text text={capitalizedText} />;
2156
+ }
2157
1905
- describe('useCallback', () => {
1906
- it('memoizes callback by comparing inputs', () => {
1907
- class IncrementButton extends React.PureComponent {
1908
- increment = () => {
1909
- this.props.increment();
1910
- };
1911
- render() {
1912
- return <Text text="Increment" />;
1913
- }
1914
- }
2158
+ ReactNoop.render(<CapitalizedText text="hello" />);
2159
+ expect(Scheduler).toFlushAndYield(["Capitalize 'hello'", 'HELLO']);
2160
+ expect(ReactNoop.getChildren()).toEqual([span('HELLO')]);
2161
1916
- function Counter({incrementBy}) {
1917
- const [count, updateCount] = useState(0);
1918
- const increment = useCallback(() => updateCount(c => c + incrementBy), [
1919
- incrementBy,
1920
- ]);
1921
- return (
1922
- <>
1923
- <IncrementButton increment={increment} ref={button} />
1924
- <Text text={'Count: ' + count} />
1925
- </>
1926
- );
1927
- }
2162
+ ReactNoop.render(<CapitalizedText text="hi" />);
2163
+ expect(Scheduler).toFlushAndYield(["Capitalize 'hi'", 'HI']);
2164
+ expect(ReactNoop.getChildren()).toEqual([span('HI')]);
2165
1929
- const button = React.createRef(null);
1930
- ReactNoop.render(<Counter incrementBy={1} />);
1931
- expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']);
1932
- expect(ReactNoop.getChildren()).toEqual([
1933
- span('Increment'),
1934
- span('Count: 0'),
1935
- ]);
1936
-
1937
- act(button.current.increment);
1938
- expect(Scheduler).toHaveYielded([
1939
- // Button should not re-render, because its props haven't changed
1940
- // 'Increment',
1941
- 'Count: 1',
1942
- ]);
1943
- expect(ReactNoop.getChildren()).toEqual([
1944
- span('Increment'),
1945
- span('Count: 1'),
1946
- ]);
1947
-
1948
- // Increase the increment amount
1949
- ReactNoop.render(<Counter incrementBy={10} />);
1950
- expect(Scheduler).toFlushAndYield([
1951
- // Inputs did change this time
1952
- 'Increment',
1953
- 'Count: 1',
1954
- ]);
1955
- expect(ReactNoop.getChildren()).toEqual([
1956
- span('Increment'),
1957
- span('Count: 1'),
1958
- ]);
1959
-
1960
- // Callback should have updated
1961
- act(button.current.increment);
1962
- expect(Scheduler).toHaveYielded(['Count: 11']);
1963
- expect(ReactNoop.getChildren()).toEqual([
1964
- span('Increment'),
1965
- span('Count: 11'),
1966
- ]);
1967
- });
1968
- });
2166
+ ReactNoop.render(<CapitalizedText text="hi" />);
2167
+ expect(Scheduler).toFlushAndYield(['HI']);
2168
+ expect(ReactNoop.getChildren()).toEqual([span('HI')]);
2169
1970
- describe('useMemo', () => {
1971
- it('memoizes value by comparing to previous inputs', () => {
1972
- function CapitalizedText(props) {
1973
- const text = props.text;
1974
- const capitalizedText = useMemo(() => {
1975
- Scheduler.unstable_yieldValue(`Capitalize '${text}'`);
1976
- return text.toUpperCase();
1977
- }, [text]);
1978
- return <Text text={capitalizedText} />;
1979
- }
2170
+ ReactNoop.render(<CapitalizedText text="goodbye" />);
2171
+ expect(Scheduler).toFlushAndYield([
2172
+ "Capitalize 'goodbye'",
2173
+ 'GOODBYE',
2174
+ ]);
2175
+ expect(ReactNoop.getChildren()).toEqual([span('GOODBYE')]);
2176
+ });
2177
1981
- ReactNoop.render(<CapitalizedText text="hello" />);
1982
- expect(Scheduler).toFlushAndYield(["Capitalize 'hello'", 'HELLO']);
1983
- expect(ReactNoop.getChildren()).toEqual([span('HELLO')]);
2178
+ it('always re-computes if no inputs are provided', () => {
2179
+ function LazyCompute(props) {
2180
+ const computed = useMemo(props.compute);
2181
+ return <Text text={computed} />;
2182
+ }
2183
1985
- ReactNoop.render(<CapitalizedText text="hi" />);
1986
- expect(Scheduler).toFlushAndYield(["Capitalize 'hi'", 'HI']);
1987
- expect(ReactNoop.getChildren()).toEqual([span('HI')]);
2184
+ function computeA() {
2185
+ Scheduler.unstable_yieldValue('compute A');
2186
+ return 'A';
2187
+ }
2188
1989
- ReactNoop.render(<CapitalizedText text="hi" />);
1990
- expect(Scheduler).toFlushAndYield(['HI']);
1991
- expect(ReactNoop.getChildren()).toEqual([span('HI')]);
2189
+ function computeB() {
2190
+ Scheduler.unstable_yieldValue('compute B');
2191
+ return 'B';
2192
+ }
2193
1993
- ReactNoop.render(<CapitalizedText text="goodbye" />);
1994
- expect(Scheduler).toFlushAndYield(["Capitalize 'goodbye'", 'GOODBYE']);
1995
- expect(ReactNoop.getChildren()).toEqual([span('GOODBYE')]);
1996
- });
2194
+ ReactNoop.render(<LazyCompute compute={computeA} />);
2195
+ expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2196
1998
- it('always re-computes if no inputs are provided', () => {
1999
- function LazyCompute(props) {
2000
- const computed = useMemo(props.compute);
2001
- return <Text text={computed} />;
2002
- }
2197
+ ReactNoop.render(<LazyCompute compute={computeA} />);
2198
+ expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2199
2004
- function computeA() {
2005
- Scheduler.unstable_yieldValue('compute A');
2006
- return 'A';
2007
- }
2200
+ ReactNoop.render(<LazyCompute compute={computeA} />);
2201
+ expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2202
2009
- function computeB() {
2010
- Scheduler.unstable_yieldValue('compute B');
2011
- return 'B';
2012
- }
2203
+ ReactNoop.render(<LazyCompute compute={computeB} />);
2204
+ expect(Scheduler).toFlushAndYield(['compute B', 'B']);
2205
+ });
2206
2014
- ReactNoop.render(<LazyCompute compute={computeA} />);
2015
- expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2207
+ it('should not invoke memoized function during re-renders unless inputs change', () => {
2208
+ function LazyCompute(props) {
2209
+ const computed = useMemo(() => props.compute(props.input), [
2210
+ props.input,
2211
+ ]);
2212
+ const [count, setCount] = useState(0);
2213
+ if (count < 3) {
2214
+ setCount(count + 1);
2215
+ }
2216
+ return <Text text={computed} />;
2217
+ }
2218
2017
- ReactNoop.render(<LazyCompute compute={computeA} />);
2018
- expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2219
+ function compute(val) {
2220
+ Scheduler.unstable_yieldValue('compute ' + val);
2221
+ return val;
2222
+ }
2223
2020
- ReactNoop.render(<LazyCompute compute={computeA} />);
2021
- expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2224
+ ReactNoop.render(<LazyCompute compute={compute} input="A" />);
2225
+ expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2226
2023
- ReactNoop.render(<LazyCompute compute={computeB} />);
2024
- expect(Scheduler).toFlushAndYield(['compute B', 'B']);
2025
- });
2227
+ ReactNoop.render(<LazyCompute compute={compute} input="A" />);
2228
+ expect(Scheduler).toFlushAndYield(['A']);
2229
2027
- it('should not invoke memoized function during re-renders unless inputs change', () => {
2028
- function LazyCompute(props) {
2029
- const computed = useMemo(() => props.compute(props.input), [
2030
- props.input,
2031
- ]);
2032
- const [count, setCount] = useState(0);
2033
- if (count < 3) {
2034
- setCount(count + 1);
2035
- }
2036
- return <Text text={computed} />;
2037
- }
2230
+ ReactNoop.render(<LazyCompute compute={compute} input="B" />);
2231
+ expect(Scheduler).toFlushAndYield(['compute B', 'B']);
2232
+ });
2233
+ });
2234
2039
- function compute(val) {
2040
- Scheduler.unstable_yieldValue('compute ' + val);
2041
- return val;
2042
- }
2235
+ describe('useRef', () => {
2236
+ it('creates a ref object initialized with the provided value', () => {
2237
+ jest.useFakeTimers();
2238
+
2239
+ function useDebouncedCallback(callback, ms, inputs) {
2240
+ const timeoutID = useRef(-1);
2241
+ useEffect(() => {
2242
+ return function unmount() {
2243
+ clearTimeout(timeoutID.current);
2244
+ };
2245
+ }, []);
2246
+ const debouncedCallback = useCallback(
2247
+ (...args) => {
2248
+ clearTimeout(timeoutID.current);
2249
+ timeoutID.current = setTimeout(callback, ms, ...args);
2250
+ },
2251
+ [callback, ms],
2252
+ );
2253
+ return useCallback(debouncedCallback, inputs);
2254
+ }
2255
2044
- ReactNoop.render(<LazyCompute compute={compute} input="A" />);
2045
- expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2256
+ let ping;
2257
+ function App() {
2258
+ ping = useDebouncedCallback(
2259
+ value => {
2260
+ Scheduler.unstable_yieldValue('ping: ' + value);
2261
+ },
2262
+ 100,
2263
+ [],
2264
+ );
2265
+ return null;
2266
+ }
2267
2047
- ReactNoop.render(<LazyCompute compute={compute} input="A" />);
2048
- expect(Scheduler).toFlushAndYield(['A']);
2268
+ act(() => {
2269
+ ReactNoop.render(<App />);
2270
+ });
2271
+ expect(Scheduler).toHaveYielded([]);
2272
2050
- ReactNoop.render(<LazyCompute compute={compute} input="B" />);
2051
- expect(Scheduler).toFlushAndYield(['compute B', 'B']);
2052
- });
2053
- });
2273
+ ping(1);
2274
+ ping(2);
2275
+ ping(3);
2276
2055
- describe('useRef', () => {
2056
- it('creates a ref object initialized with the provided value', () => {
2057
- jest.useFakeTimers();
2277
+ expect(Scheduler).toHaveYielded([]);
2278
2059
- function useDebouncedCallback(callback, ms, inputs) {
2060
- const timeoutID = useRef(-1);
2061
- useEffect(() => {
2062
- return function unmount() {
2063
- clearTimeout(timeoutID.current);
2064
- };
2065
- }, []);
2066
- const debouncedCallback = useCallback(
2067
- (...args) => {
2068
- clearTimeout(timeoutID.current);
2069
- timeoutID.current = setTimeout(callback, ms, ...args);
2070
- },
2071
- [callback, ms],
2072
- );
2073
- return useCallback(debouncedCallback, inputs);
2074
- }
2279
+ jest.advanceTimersByTime(100);
2280
2076
- let ping;
2077
- function App() {
2078
- ping = useDebouncedCallback(
2079
- value => {
2080
- Scheduler.unstable_yieldValue('ping: ' + value);
2081
- },
2082
- 100,
2083
- [],
2084
- );
2085
- return null;
2086
- }
2281
+ expect(Scheduler).toHaveYielded(['ping: 3']);
2282
2088
- act(() => {
2089
- ReactNoop.render(<App />);
2090
- });
2091
- expect(Scheduler).toHaveYielded([]);
2283
+ ping(4);
2284
+ jest.advanceTimersByTime(20);
2285
+ ping(5);
2286
+ ping(6);
2287
+ jest.advanceTimersByTime(80);
2288
2093
- ping(1);
2094
- ping(2);
2095
- ping(3);
2289
+ expect(Scheduler).toHaveYielded([]);
2290
2097
- expect(Scheduler).toHaveYielded([]);
2291
+ jest.advanceTimersByTime(20);
2292
+ expect(Scheduler).toHaveYielded(['ping: 6']);
2293
+ });
2294
2099
- jest.advanceTimersByTime(100);
2295
+ it('should return the same ref during re-renders', () => {
2296
+ function Counter() {
2297
+ const ref = useRef('val');
2298
+ const [count, setCount] = useState(0);
2299
+ const [firstRef] = useState(ref);
2300
2101
- expect(Scheduler).toHaveYielded(['ping: 3']);
2301
+ if (firstRef !== ref) {
2302
+ throw new Error('should never change');
2303
+ }
2304
2103
- ping(4);
2104
- jest.advanceTimersByTime(20);
2105
- ping(5);
2106
- ping(6);
2107
- jest.advanceTimersByTime(80);
2305
+ if (count < 3) {
2306
+ setCount(count + 1);
2307
+ }
2308
2109
- expect(Scheduler).toHaveYielded([]);
2309
+ return <Text text={ref.current} />;
2310
+ }
2311
2111
- jest.advanceTimersByTime(20);
2112
- expect(Scheduler).toHaveYielded(['ping: 6']);
2113
- });
2312
+ ReactNoop.render(<Counter />);
2313
+ expect(Scheduler).toFlushAndYield(['val']);
2314
2115
- it('should return the same ref during re-renders', () => {
2116
- function Counter() {
2117
- const ref = useRef('val');
2118
- const [count, setCount] = useState(0);
2119
- const [firstRef] = useState(ref);
2315
+ ReactNoop.render(<Counter />);
2316
+ expect(Scheduler).toFlushAndYield(['val']);
2317
+ });
2318
+ });
2319
2121
- if (firstRef !== ref) {
2122
- throw new Error('should never change');
2123
- }
2320
+ describe('useImperativeHandle', () => {
2321
+ it('does not update when deps are the same', () => {
2322
+ const INCREMENT = 'INCREMENT';
2323
2125
- if (count < 3) {
2126
- setCount(count + 1);
2127
- }
2324
+ function reducer(state, action) {
2325
+ return action === INCREMENT ? state + 1 : state;
2326
+ }
2327
2129
- return <Text text={ref.current} />;
2130
- }
2328
+ function Counter(props, ref) {
2329
+ const [count, dispatch] = useReducer(reducer, 0);
2330
+ useImperativeHandle(ref, () => ({count, dispatch}), []);
2331
+ return <Text text={'Count: ' + count} />;
2332
+ }
2333
2132
- ReactNoop.render(<Counter />);
2133
- expect(Scheduler).toFlushAndYield(['val']);
2334
+ Counter = forwardRef(Counter);
2335
+ const counter = React.createRef(null);
2336
+ ReactNoop.render(<Counter ref={counter} />);
2337
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
2338
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2339
+ expect(counter.current.count).toBe(0);
2340
2135
- ReactNoop.render(<Counter />);
2136
- expect(Scheduler).toFlushAndYield(['val']);
2137
- });
2138
- });
2341
+ act(() => {
2342
+ counter.current.dispatch(INCREMENT);
2343
+ });
2344
+ expect(Scheduler).toHaveYielded(['Count: 1']);
2345
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2346
+ // Intentionally not updated because of [] deps:
2347
+ expect(counter.current.count).toBe(0);
2348
+ });
2349
2140
- describe('useImperativeHandle', () => {
2141
- it('does not update when deps are the same', () => {
2142
- const INCREMENT = 'INCREMENT';
2350
+ // Regression test for https://github.com/facebook/react/issues/14782
2351
+ it('automatically updates when deps are not specified', () => {
2352
+ const INCREMENT = 'INCREMENT';
2353
2144
- function reducer(state, action) {
2145
- return action === INCREMENT ? state + 1 : state;
2146
- }
2354
+ function reducer(state, action) {
2355
+ return action === INCREMENT ? state + 1 : state;
2356
+ }
2357
2148
- function Counter(props, ref) {
2149
- const [count, dispatch] = useReducer(reducer, 0);
2150
- useImperativeHandle(ref, () => ({count, dispatch}), []);
2151
- return <Text text={'Count: ' + count} />;
2152
- }
2358
+ function Counter(props, ref) {
2359
+ const [count, dispatch] = useReducer(reducer, 0);
2360
+ useImperativeHandle(ref, () => ({count, dispatch}));
2361
+ return <Text text={'Count: ' + count} />;
2362
+ }
2363
2154
- Counter = forwardRef(Counter);
2155
- const counter = React.createRef(null);
2156
- ReactNoop.render(<Counter ref={counter} />);
2157
- expect(Scheduler).toFlushAndYield(['Count: 0']);
2158
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2159
- expect(counter.current.count).toBe(0);
2364
+ Counter = forwardRef(Counter);
2365
+ const counter = React.createRef(null);
2366
+ ReactNoop.render(<Counter ref={counter} />);
2367
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
2368
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2369
+ expect(counter.current.count).toBe(0);
2370
2161
- act(() => {
2162
- counter.current.dispatch(INCREMENT);
2163
- });
2164
- expect(Scheduler).toHaveYielded(['Count: 1']);
2165
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2166
- // Intentionally not updated because of [] deps:
2167
- expect(counter.current.count).toBe(0);
2168
- });
2371
+ act(() => {
2372
+ counter.current.dispatch(INCREMENT);
2373
+ });
2374
+ expect(Scheduler).toHaveYielded(['Count: 1']);
2375
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2376
+ expect(counter.current.count).toBe(1);
2377
+ });
2378
2170
- // Regression test for https://github.com/facebook/react/issues/14782
2171
- it('automatically updates when deps are not specified', () => {
2172
- const INCREMENT = 'INCREMENT';
2379
+ it('updates when deps are different', () => {
2380
+ const INCREMENT = 'INCREMENT';
2381
2174
- function reducer(state, action) {
2175
- return action === INCREMENT ? state + 1 : state;
2176
- }
2382
+ function reducer(state, action) {
2383
+ return action === INCREMENT ? state + 1 : state;
2384
+ }
2385
2178
- function Counter(props, ref) {
2179
- const [count, dispatch] = useReducer(reducer, 0);
2180
- useImperativeHandle(ref, () => ({count, dispatch}));
2181
- return <Text text={'Count: ' + count} />;
2182
- }
2386
+ let totalRefUpdates = 0;
2387
+ function Counter(props, ref) {
2388
+ const [count, dispatch] = useReducer(reducer, 0);
2389
+ useImperativeHandle(
2390
+ ref,
2391
+ () => {
2392
+ totalRefUpdates++;
2393
+ return {count, dispatch};
2394
+ },
2395
+ [count],
2396
+ );
2397
+ return <Text text={'Count: ' + count} />;
2398
+ }
2399
2184
- Counter = forwardRef(Counter);
2185
- const counter = React.createRef(null);
2186
- ReactNoop.render(<Counter ref={counter} />);
2187
- expect(Scheduler).toFlushAndYield(['Count: 0']);
2188
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2189
- expect(counter.current.count).toBe(0);
2400
+ Counter = forwardRef(Counter);
2401
+ const counter = React.createRef(null);
2402
+ ReactNoop.render(<Counter ref={counter} />);
2403
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
2404
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2405
+ expect(counter.current.count).toBe(0);
2406
+ expect(totalRefUpdates).toBe(1);
2407
2191
- act(() => {
2192
- counter.current.dispatch(INCREMENT);
2408
+ act(() => {
2409
+ counter.current.dispatch(INCREMENT);
2410
+ });
2411
+ expect(Scheduler).toHaveYielded(['Count: 1']);
2412
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2413
+ expect(counter.current.count).toBe(1);
2414
+ expect(totalRefUpdates).toBe(2);
2415
+
2416
+ // Update that doesn't change the ref dependencies
2417
+ ReactNoop.render(<Counter ref={counter} />);
2418
+ expect(Scheduler).toFlushAndYield(['Count: 1']);
2419
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2420
+ expect(counter.current.count).toBe(1);
2421
+ expect(totalRefUpdates).toBe(2); // Should not increase since last time
2422
+ });
2423
});
2194
- expect(Scheduler).toHaveYielded(['Count: 1']);
2195
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2196
- expect(counter.current.count).toBe(1);
2197
- });
2424
+ describe('useTransition', () => {
2425
+ it.experimental(
2426
+ 'delays showing loading state until after timeout',
2427
+ async () => {
2428
+ let transition;
2429
+ function App() {
2430
+ const [show, setShow] = useState(false);
2431
+ const [startTransition, isPending] = useTransition({
2432
+ timeoutMs: 1000,
2433
+ });
2434
+ transition = () => {
2435
+ startTransition(() => {
2436
+ setShow(true);
2437
+ });
2438
+ };
2439
+ return (
2440
+ <Suspense
2441
+ fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
2442
+ {show ? (
2443
+ <AsyncText
2444
+ ms={2000}
2445
+ text={`After... Pending: ${isPending}`}
2446
+ />
2447
+ ) : (
2448
+ <Text text={`Before... Pending: ${isPending}`} />
2449
+ )}
2450
+ </Suspense>
2451
+ );
2452
+ }
2453
+ ReactNoop.render(<App />);
2454
+ expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
2455
+ expect(ReactNoop.getChildren()).toEqual([
2456
+ span('Before... Pending: false'),
2457
+ ]);
2458
+
2459
+ act(() => {
2460
+ Scheduler.unstable_runWithPriority(
2461
+ Scheduler.unstable_UserBlockingPriority,
2462
+ transition,
2463
+ );
2464
+ });
2465
+ Scheduler.unstable_advanceTime(500);
2466
+ await advanceTimers(500);
2467
+ expect(Scheduler).toHaveYielded([
2468
+ 'Before... Pending: true',
2469
+ 'Suspend! [After... Pending: false]',
2470
+ 'Loading... Pending: false',
2471
+ ]);
2472
+ expect(ReactNoop.getChildren()).toEqual([
2473
+ span('Before... Pending: true'),
2474
+ ]);
2475
+
2476
+ Scheduler.unstable_advanceTime(1000);
2477
+ await advanceTimers(1000);
2478
+ expect(ReactNoop.getChildren()).toEqual([
2479
+ hiddenSpan('Before... Pending: true'),
2480
+ span('Loading... Pending: false'),
2481
+ ]);
2482
+
2483
+ Scheduler.unstable_advanceTime(500);
2484
+ await advanceTimers(500);
2485
+ expect(Scheduler).toHaveYielded([
2486
+ 'Promise resolved [After... Pending: false]',
2487
+ ]);
2488
+ expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2489
+ expect(ReactNoop.getChildren()).toEqual([
2490
+ span('After... Pending: false'),
2491
+ ]);
2492
+ },
2493
+ );
2494
+ it.experimental(
2495
+ 'delays showing loading state until after busyDelayMs + busyMinDurationMs',
2496
+ async () => {
2497
+ let transition;
2498
+ function App() {
2499
+ const [show, setShow] = useState(false);
2500
+ const [startTransition, isPending] = useTransition({
2501
+ busyDelayMs: 1000,
2502
+ busyMinDurationMs: 2000,
2503
+ });
2504
+ transition = () => {
2505
+ startTransition(() => {
2506
+ setShow(true);
2507
+ });
2508
+ };
2509
+ return (
2510
+ <Suspense
2511
+ fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
2512
+ {show ? (
2513
+ <AsyncText
2514
+ ms={2000}
2515
+ text={`After... Pending: ${isPending}`}
2516
+ />
2517
+ ) : (
2518
+ <Text text={`Before... Pending: ${isPending}`} />
2519
+ )}
2520
+ </Suspense>
2521
+ );
2522
+ }
2523
+ ReactNoop.render(<App />);
2524
+ expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
2525
+ expect(ReactNoop.getChildren()).toEqual([
2526
+ span('Before... Pending: false'),
2527
+ ]);
2528
+
2529
+ act(() => {
2530
+ Scheduler.unstable_runWithPriority(
2531
+ Scheduler.unstable_UserBlockingPriority,
2532
+ transition,
2533
+ );
2534
+ });
2535
+ Scheduler.unstable_advanceTime(1000);
2536
+ await advanceTimers(1000);
2537
+ expect(Scheduler).toHaveYielded([
2538
+ 'Before... Pending: true',
2539
+ 'Suspend! [After... Pending: false]',
2540
+ 'Loading... Pending: false',
2541
+ ]);
2542
+ expect(ReactNoop.getChildren()).toEqual([
2543
+ span('Before... Pending: true'),
2544
+ ]);
2545
+
2546
+ Scheduler.unstable_advanceTime(1000);
2547
+ await advanceTimers(1000);
2548
+ expect(Scheduler).toHaveYielded([
2549
+ 'Promise resolved [After... Pending: false]',
2550
+ ]);
2551
+ expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2552
+ expect(ReactNoop.getChildren()).toEqual([
2553
+ span('Before... Pending: true'),
2554
+ ]);
2555
+
2556
+ Scheduler.unstable_advanceTime(1000);
2557
+ await advanceTimers(1000);
2558
+ expect(ReactNoop.getChildren()).toEqual([
2559
+ span('Before... Pending: true'),
2560
+ ]);
2561
+ Scheduler.unstable_advanceTime(250);
2562
+ await advanceTimers(250);
2563
+ expect(ReactNoop.getChildren()).toEqual([
2564
+ span('After... Pending: false'),
2565
+ ]);
2566
+ },
2567
+ );
2568
+ });
2569
+ describe('useDeferredValue', () => {
2570
+ it.experimental(
2571
+ 'defers text value until specified timeout',
2572
+ async () => {
2573
+ function TextBox({text}) {
2574
+ return <AsyncText ms={1000} text={text} />;
2575
+ }
2576
+
2577
+ let _setText;
2578
+ function App() {
2579
+ const [text, setText] = useState('A');
2580
+ const deferredText = useDeferredValue(text, {
2581
+ timeoutMs: 500,
2582
+ });
2583
+ _setText = setText;
2584
+ return (
2585
+ <>
2586
+ <Text text={text} />
2587
+ <Suspense fallback={<Text text={'Loading'} />}>
2588
+ <TextBox text={deferredText} />
2589
+ </Suspense>
2590
+ </>
2591
+ );
2592
+ }
2593
+
2594
+ act(() => {
2595
+ ReactNoop.render(<App />);
2596
+ });
2597
2199
- it('updates when deps are different', () => {
2200
- const INCREMENT = 'INCREMENT';
2598
+ expect(Scheduler).toHaveYielded(['A', 'Suspend! [A]', 'Loading']);
2599
+ expect(ReactNoop.getChildren()).toEqual([
2600
+ span('A'),
2601
+ span('Loading'),
2602
+ ]);
2603
2202
- function reducer(state, action) {
2203
- return action === INCREMENT ? state + 1 : state;
2204
- }
2604
+ Scheduler.unstable_advanceTime(1000);
2605
+ await advanceTimers(1000);
2606
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2607
+ expect(Scheduler).toFlushAndYield(['A']);
2608
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('A')]);
2609
2206
- let totalRefUpdates = 0;
2207
- function Counter(props, ref) {
2208
- const [count, dispatch] = useReducer(reducer, 0);
2209
- useImperativeHandle(
2210
- ref,
2211
- () => {
2212
- totalRefUpdates++;
2213
- return {count, dispatch};
2610
+ act(() => {
2611
+ _setText('B');
2612
+ });
2613
+ expect(Scheduler).toHaveYielded([
2614
+ 'B',
2615
+ 'A',
2616
+ 'B',
2617
+ 'Suspend! [B]',
2618
+ 'Loading',
2619
+ ]);
2620
+ expect(Scheduler).toFlushAndYield([]);
2621
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2622
+
2623
+ Scheduler.unstable_advanceTime(250);
2624
+ await advanceTimers(250);
2625
+ expect(Scheduler).toFlushAndYield([]);
2626
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2627
+
2628
+ Scheduler.unstable_advanceTime(500);
2629
+ await advanceTimers(500);
2630
+ expect(ReactNoop.getChildren()).toEqual([
2631
+ span('B'),
2632
+ hiddenSpan('A'),
2633
+ span('Loading'),
2634
+ ]);
2635
+
2636
+ Scheduler.unstable_advanceTime(250);
2637
+ await advanceTimers(250);
2638
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2639
+
2640
+ act(() => {
2641
+ expect(Scheduler).toFlushAndYield(['B']);
2642
+ });
2643
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('B')]);
2644
},
2215
- [count],
2645
);
2217
- return <Text text={'Count: ' + count} />;
2218
- }
2219
-
2220
- Counter = forwardRef(Counter);
2221
- const counter = React.createRef(null);
2222
- ReactNoop.render(<Counter ref={counter} />);
2223
- expect(Scheduler).toFlushAndYield(['Count: 0']);
2224
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2225
- expect(counter.current.count).toBe(0);
2226
- expect(totalRefUpdates).toBe(1);
2227
-
2228
- act(() => {
2229
- counter.current.dispatch(INCREMENT);
2646
});
2231
- expect(Scheduler).toHaveYielded(['Count: 1']);
2232
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2233
- expect(counter.current.count).toBe(1);
2234
- expect(totalRefUpdates).toBe(2);
2235
-
2236
- // Update that doesn't change the ref dependencies
2237
- ReactNoop.render(<Counter ref={counter} />);
2238
- expect(Scheduler).toFlushAndYield(['Count: 1']);
2239
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2240
- expect(counter.current.count).toBe(1);
2241
- expect(totalRefUpdates).toBe(2); // Should not increase since last time
2242
- });
2243
- });
2244
- describe('useTransition', () => {
2245
- it.experimental(
2246
- 'delays showing loading state until after timeout',
2247
- async () => {
2248
- let transition;
2249
- function App() {
2250
- const [show, setShow] = useState(false);
2251
- const [startTransition, isPending] = useTransition({
2252
- timeoutMs: 1000,
2253
- });
2254
- transition = () => {
2255
- startTransition(() => {
2256
- setShow(true);
2257
- });
2258
- };
2259
- return (
2260
- <Suspense
2261
- fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
2262
- {show ? (
2263
- <AsyncText ms={2000} text={`After... Pending: ${isPending}`} />
2264
- ) : (
2265
- <Text text={`Before... Pending: ${isPending}`} />
2266
- )}
2267
- </Suspense>
2268
- );
2269
- }
2270
- ReactNoop.render(<App />);
2271
- expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
2272
- expect(ReactNoop.getChildren()).toEqual([
2273
- span('Before... Pending: false'),
2274
- ]);
2647
2276
- act(() => {
2277
- Scheduler.unstable_runWithPriority(
2278
- Scheduler.unstable_UserBlockingPriority,
2279
- transition,
2280
- );
2281
- });
2282
- Scheduler.unstable_advanceTime(500);
2283
- await advanceTimers(500);
2284
- expect(Scheduler).toHaveYielded([
2285
- 'Before... Pending: true',
2286
- 'Suspend! [After... Pending: false]',
2287
- 'Loading... Pending: false',
2288
- ]);
2289
- expect(ReactNoop.getChildren()).toEqual([
2290
- span('Before... Pending: true'),
2291
- ]);
2648
+ describe('progressive enhancement (not supported)', () => {
2649
+ it('mount additional state', () => {
2650
+ let updateA;
2651
+ let updateB;
2652
+ // let updateC;
2653
+
2654
+ function App(props) {
2655
+ const [A, _updateA] = useState(0);
2656
+ const [B, _updateB] = useState(0);
2657
+ updateA = _updateA;
2658
+ updateB = _updateB;
2659
+
2660
+ let C;
2661
+ if (props.loadC) {
2662
+ useState(0);
2663
+ } else {
2664
+ C = '[not loaded]';
2665
+ }
2666
+
2667
+ return <Text text={`A: ${A}, B: ${B}, C: ${C}`} />;
2668
+ }
2669
2293
- Scheduler.unstable_advanceTime(1000);
2294
- await advanceTimers(1000);
2295
- expect(ReactNoop.getChildren()).toEqual([
2296
- hiddenSpan('Before... Pending: true'),
2297
- span('Loading... Pending: false'),
2298
- ]);
2670
+ ReactNoop.render(<App loadC={false} />);
2671
+ expect(Scheduler).toFlushAndYield(['A: 0, B: 0, C: [not loaded]']);
2672
+ expect(ReactNoop.getChildren()).toEqual([
2673
+ span('A: 0, B: 0, C: [not loaded]'),
2674
+ ]);
2675
2300
- Scheduler.unstable_advanceTime(500);
2301
- await advanceTimers(500);
2302
- expect(Scheduler).toHaveYielded([
2303
- 'Promise resolved [After... Pending: false]',
2304
- ]);
2305
- expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2306
- expect(ReactNoop.getChildren()).toEqual([
2307
- span('After... Pending: false'),
2308
- ]);
2309
- },
2310
- );
2311
- it.experimental(
2312
- 'delays showing loading state until after busyDelayMs + busyMinDurationMs',
2313
- async () => {
2314
- let transition;
2315
- function App() {
2316
- const [show, setShow] = useState(false);
2317
- const [startTransition, isPending] = useTransition({
2318
- busyDelayMs: 1000,
2319
- busyMinDurationMs: 2000,
2676
+ act(() => {
2677
+ updateA(2);
2678
+ updateB(3);
2679
});
2321
- transition = () => {
2322
- startTransition(() => {
2323
- setShow(true);
2324
- });
2325
- };
2326
- return (
2327
- <Suspense
2328
- fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
2329
- {show ? (
2330
- <AsyncText ms={2000} text={`After... Pending: ${isPending}`} />
2331
- ) : (
2332
- <Text text={`Before... Pending: ${isPending}`} />
2333
- )}
2334
- </Suspense>
2335
- );
2336
- }
2337
- ReactNoop.render(<App />);
2338
- expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
2339
- expect(ReactNoop.getChildren()).toEqual([
2340
- span('Before... Pending: false'),
2341
- ]);
2680
2343
- act(() => {
2344
- Scheduler.unstable_runWithPriority(
2345
- Scheduler.unstable_UserBlockingPriority,
2346
- transition,
2347
- );
2348
- });
2349
- Scheduler.unstable_advanceTime(1000);
2350
- await advanceTimers(1000);
2351
- expect(Scheduler).toHaveYielded([
2352
- 'Before... Pending: true',
2353
- 'Suspend! [After... Pending: false]',
2354
- 'Loading... Pending: false',
2355
- ]);
2356
- expect(ReactNoop.getChildren()).toEqual([
2357
- span('Before... Pending: true'),
2358
- ]);
2681
+ expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: [not loaded]']);
2682
+ expect(ReactNoop.getChildren()).toEqual([
2683
+ span('A: 2, B: 3, C: [not loaded]'),
2684
+ ]);
2685
2360
- Scheduler.unstable_advanceTime(1000);
2361
- await advanceTimers(1000);
2362
- expect(Scheduler).toHaveYielded([
2363
- 'Promise resolved [After... Pending: false]',
2364
- ]);
2365
- expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2366
- expect(ReactNoop.getChildren()).toEqual([
2367
- span('Before... Pending: true'),
2368
- ]);
2686
+ ReactNoop.render(<App loadC={true} />);
2687
+ expect(() => {
2688
+ expect(() => {
2689
+ expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 0']);
2690
+ }).toThrow('Rendered more hooks than during the previous render');
2691
+ }).toErrorDev([
2692
+ 'Warning: React has detected a change in the order of Hooks called by App. ' +
2693
+ 'This will lead to bugs and errors if not fixed. For more information, ' +
2694
+ 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
2695
+ ' Previous render Next render\n' +
2696
+ ' ------------------------------------------------------\n' +
2697
+ '1. useState useState\n' +
2698
+ '2. useState useState\n' +
2699
+ '3. undefined useState\n' +
2700
+ ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
2701
+ ]);
2702
2370
- Scheduler.unstable_advanceTime(1000);
2371
- await advanceTimers(1000);
2372
- expect(ReactNoop.getChildren()).toEqual([
2373
- span('Before... Pending: true'),
2374
- ]);
2375
- Scheduler.unstable_advanceTime(250);
2376
- await advanceTimers(250);
2377
- expect(ReactNoop.getChildren()).toEqual([
2378
- span('After... Pending: false'),
2379
- ]);
2380
- },
2381
- );
2382
- });
2383
- describe('useDeferredValue', () => {
2384
- it.experimental('defers text value until specified timeout', async () => {
2385
- function TextBox({text}) {
2386
- return <AsyncText ms={1000} text={text} />;
2387
- }
2703
+ // Uncomment if/when we support this again
2704
+ // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 0')]);
2705
2389
- let _setText;
2390
- function App() {
2391
- const [text, setText] = useState('A');
2392
- const deferredText = useDeferredValue(text, {
2393
- timeoutMs: 500,
2706
+ // updateC(4);
2707
+ // expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 4']);
2708
+ // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]);
2709
});
2395
- _setText = setText;
2396
- return (
2397
- <>
2398
- <Text text={text} />
2399
- <Suspense fallback={<Text text={'Loading'} />}>
2400
- <TextBox text={deferredText} />
2401
- </Suspense>
2402
- </>
2403
- );
2404
- }
2710
2406
- act(() => {
2407
- ReactNoop.render(<App />);
2408
- });
2711
+ it('unmount state', () => {
2712
+ let updateA;
2713
+ let updateB;
2714
+ let updateC;
2715
+
2716
+ function App(props) {
2717
+ const [A, _updateA] = useState(0);
2718
+ const [B, _updateB] = useState(0);
2719
+ updateA = _updateA;
2720
+ updateB = _updateB;
2721
+
2722
+ let C;
2723
+ if (props.loadC) {
2724
+ const [_C, _updateC] = useState(0);
2725
+ C = _C;
2726
+ updateC = _updateC;
2727
+ } else {
2728
+ C = '[not loaded]';
2729
+ }
2730
+
2731
+ return <Text text={`A: ${A}, B: ${B}, C: ${C}`} />;
2732
+ }
2733
2410
- expect(Scheduler).toHaveYielded(['A', 'Suspend! [A]', 'Loading']);
2411
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading')]);
2734
+ ReactNoop.render(<App loadC={true} />);
2735
+ expect(Scheduler).toFlushAndYield(['A: 0, B: 0, C: 0']);
2736
+ expect(ReactNoop.getChildren()).toEqual([span('A: 0, B: 0, C: 0')]);
2737
+ act(() => {
2738
+ updateA(2);
2739
+ updateB(3);
2740
+ updateC(4);
2741
+ });
2742
+ expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: 4']);
2743
+ expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]);
2744
+ ReactNoop.render(<App loadC={false} />);
2745
+ expect(Scheduler).toFlushAndThrow(
2746
+ 'Rendered fewer hooks than expected. This may be caused by an ' +
2747
+ 'accidental early return statement.',
2748
+ );
2749
+ });
2750
2413
- Scheduler.unstable_advanceTime(1000);
2414
- await advanceTimers(1000);
2415
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2416
- expect(Scheduler).toFlushAndYield(['A']);
2417
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('A')]);
2751
+ it('unmount effects', () => {
2752
+ function App(props) {
2753
+ useEffect(() => {
2754
+ Scheduler.unstable_yieldValue('Mount A');
2755
+ return () => {
2756
+ Scheduler.unstable_yieldValue('Unmount A');
2757
+ };
2758
+ }, []);
2759
+
2760
+ if (props.showMore) {
2761
+ useEffect(() => {
2762
+ Scheduler.unstable_yieldValue('Mount B');
2763
+ return () => {
2764
+ Scheduler.unstable_yieldValue('Unmount B');
2765
+ };
2766
+ }, []);
2767
+ }
2768
2419
- act(() => {
2420
- _setText('B');
2421
- });
2422
- expect(Scheduler).toHaveYielded([
2423
- 'B',
2424
- 'A',
2425
- 'B',
2426
- 'Suspend! [B]',
2427
- 'Loading',
2428
- ]);
2429
- expect(Scheduler).toFlushAndYield([]);
2430
- expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2431
-
2432
- Scheduler.unstable_advanceTime(250);
2433
- await advanceTimers(250);
2434
- expect(Scheduler).toFlushAndYield([]);
2435
- expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2436
-
2437
- Scheduler.unstable_advanceTime(500);
2438
- await advanceTimers(500);
2439
- expect(ReactNoop.getChildren()).toEqual([
2440
- span('B'),
2441
- hiddenSpan('A'),
2442
- span('Loading'),
2443
- ]);
2444
-
2445
- Scheduler.unstable_advanceTime(250);
2446
- await advanceTimers(250);
2447
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2448
-
2449
- act(() => {
2450
- expect(Scheduler).toFlushAndYield(['B']);
2451
- });
2452
- expect(ReactNoop.getChildren()).toEqual([span('B'), span('B')]);
2453
- });
2454
- });
2769
+ return null;
2770
+ }
2771
2456
- describe('progressive enhancement (not supported)', () => {
2457
- it('mount additional state', () => {
2458
- let updateA;
2459
- let updateB;
2460
- // let updateC;
2461
-
2462
- function App(props) {
2463
- const [A, _updateA] = useState(0);
2464
- const [B, _updateB] = useState(0);
2465
- updateA = _updateA;
2466
- updateB = _updateB;
2467
-
2468
- let C;
2469
- if (props.loadC) {
2470
- useState(0);
2471
- } else {
2472
- C = '[not loaded]';
2473
- }
2772
+ act(() => {
2773
+ ReactNoop.render(<App showMore={false} />, () =>
2774
+ Scheduler.unstable_yieldValue('Sync effect'),
2775
+ );
2776
+ expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
2777
+ });
2778
2475
- return <Text text={`A: ${A}, B: ${B}, C: ${C}`} />;
2476
- }
2779
+ expect(Scheduler).toHaveYielded(['Mount A']);
2780
+
2781
+ act(() => {
2782
+ ReactNoop.render(<App showMore={true} />);
2783
+ expect(() => {
2784
+ expect(() => {
2785
+ expect(Scheduler).toFlushAndYield([]);
2786
+ }).toThrow('Rendered more hooks than during the previous render');
2787
+ }).toErrorDev([
2788
+ 'Warning: React has detected a change in the order of Hooks called by App. ' +
2789
+ 'This will lead to bugs and errors if not fixed. For more information, ' +
2790
+ 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
2791
+ ' Previous render Next render\n' +
2792
+ ' ------------------------------------------------------\n' +
2793
+ '1. useEffect useEffect\n' +
2794
+ '2. undefined useEffect\n' +
2795
+ ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
2796
+ ]);
2797
+ });
2798
2478
- ReactNoop.render(<App loadC={false} />);
2479
- expect(Scheduler).toFlushAndYield(['A: 0, B: 0, C: [not loaded]']);
2480
- expect(ReactNoop.getChildren()).toEqual([
2481
- span('A: 0, B: 0, C: [not loaded]'),
2482
- ]);
2799
+ // Uncomment if/when we support this again
2800
+ // ReactNoop.flushPassiveEffects();
2801
+ // expect(Scheduler).toHaveYielded(['Mount B']);
2802
2484
- act(() => {
2485
- updateA(2);
2486
- updateB(3);
2803
+ // ReactNoop.render(<App showMore={false} />);
2804
+ // expect(Scheduler).toFlushAndThrow(
2805
+ // 'Rendered fewer hooks than expected. This may be caused by an ' +
2806
+ // 'accidental early return statement.',
2807
+ // );
2808
+ });
2809
});
2810
2489
- expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: [not loaded]']);
2490
- expect(ReactNoop.getChildren()).toEqual([
2491
- span('A: 2, B: 3, C: [not loaded]'),
2492
- ]);
2493
-
2494
- ReactNoop.render(<App loadC={true} />);
2495
- expect(() => {
2496
- expect(() => {
2497
- expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 0']);
2498
- }).toThrow('Rendered more hooks than during the previous render');
2499
- }).toErrorDev([
2500
- 'Warning: React has detected a change in the order of Hooks called by App. ' +
2501
- 'This will lead to bugs and errors if not fixed. For more information, ' +
2502
- 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' +
2503
- ' Previous render Next render\n' +
2504
- ' ------------------------------------------------------\n' +
2505
- '1. useState useState\n' +
2506
- '2. useState useState\n' +
2507
- '3. undefined useState\n' +
2508
- ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
2509
- ]);
2510
-
2511
- // Uncomment if/when we support this again
2512
- // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 0')]);
2513
-
2514
- // updateC(4);
2515
- // expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 4']);
2516
- // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]);
2517
- });
2811
+ it('eager bailout optimization should always compare to latest rendered reducer', () => {
This file is too large to show in full.
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+2709
-2624
@@ -9,1230 +9,1585 @@ let Suspense;
9
let TextResource;
10
let textResourceShouldFail;
11
12
-describe('ReactSuspenseWithNoopRenderer', () => {
13
- if (!__EXPERIMENTAL__) {
14
- it("empty test so Jest doesn't complain", () => {});
15
- return;
16
- }
17
-
18
- beforeEach(() => {
19
- jest.resetModules();
20
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
- ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22
- ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
23
- ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
24
- ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = true;
25
- React = require('react');
26
- Fragment = React.Fragment;
27
- ReactNoop = require('react-noop-renderer');
28
- Scheduler = require('scheduler');
29
- ReactCache = require('react-cache');
30
- Suspense = React.Suspense;
31
-
32
- TextResource = ReactCache.unstable_createResource(
33
- ([text, ms = 0]) => {
34
- return new Promise((resolve, reject) =>
35
- setTimeout(() => {
36
- if (textResourceShouldFail) {
37
- Scheduler.unstable_yieldValue(`Promise rejected [${text}]`);
38
- reject(new Error('Failed to load: ' + text));
39
- } else {
40
- Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
41
- resolve(text);
42
- }
43
- }, ms),
44
- );
45
- },
46
- ([text, ms]) => text,
47
- );
48
- textResourceShouldFail = false;
49
- });
50
-
51
- // function div(...children) {
52
- // children = children.map(
53
- // c => (typeof c === 'string' ? {text: c, hidden: false} : c),
54
- // );
55
- // return {type: 'div', children, prop: undefined, hidden: false};
56
- // }
57
-
58
- function span(prop) {
59
- return {type: 'span', children: [], prop, hidden: false};
60
- }
61
-
62
- function hiddenSpan(prop) {
63
- return {type: 'span', children: [], prop, hidden: true};
64
- }
65
-
66
- function advanceTimers(ms) {
67
- // Note: This advances Jest's virtual time but not React's. Use
68
- // ReactNoop.expire for that.
69
- if (typeof ms !== 'number') {
70
- throw new Error('Must specify ms');
71
- }
72
- jest.advanceTimersByTime(ms);
73
- // Wait until the end of the current tick
74
- // We cannot use a timer since we're faking them
75
- return Promise.resolve().then(() => {});
76
- }
77
-
78
- function Text(props) {
79
- Scheduler.unstable_yieldValue(props.text);
80
- return <span prop={props.text} ref={props.hostRef} />;
81
- }
82
-
83
- function AsyncText(props) {
84
- const text = props.text;
85
- try {
86
- TextResource.read([props.text, props.ms]);
87
- Scheduler.unstable_yieldValue(text);
88
- return <span prop={text} />;
89
- } catch (promise) {
90
- if (typeof promise.then === 'function') {
91
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
92
- } else {
93
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
94
- }
95
- throw promise;
96
- }
97
- }
98
-
99
- it('warns if the deprecated maxDuration option is used', () => {
100
- function Foo() {
101
- return (
102
- <Suspense maxDuration={100} fallback="Loading...">
103
- <div />;
104
- </Suspense>
12
+function loadModules({
13
+ deferPassiveEffectCleanupDuringUnmount,
14
+ runAllPassiveEffectDestroysBeforeCreates,
15
+}) {
16
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
17
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
18
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
19
+ ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
20
+ ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = deferPassiveEffectCleanupDuringUnmount;
21
+ ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates = runAllPassiveEffectDestroysBeforeCreates;
22
+ React = require('react');
23
+ Fragment = React.Fragment;
24
+ ReactNoop = require('react-noop-renderer');
25
+ Scheduler = require('scheduler');
26
+ ReactCache = require('react-cache');
27
+ Suspense = React.Suspense;
28
+
29
+ TextResource = ReactCache.unstable_createResource(
30
+ ([text, ms = 0]) => {
31
+ return new Promise((resolve, reject) =>
32
+ setTimeout(() => {
33
+ if (textResourceShouldFail) {
34
+ Scheduler.unstable_yieldValue(`Promise rejected [${text}]`);
35
+ reject(new Error('Failed to load: ' + text));
36
+ } else {
37
+ Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
38
+ resolve(text);
39
+ }
40
+ }, ms),
41
);
106
- }
107
-
108
- ReactNoop.render(<Foo />);
109
-
110
- expect(() => Scheduler.unstable_flushAll()).toErrorDev([
111
- 'Warning: maxDuration has been removed from React. ' +
112
- 'Remove the maxDuration prop.' +
113
- '\n in Suspense (at **)' +
114
- '\n in Foo (at **)',
115
- ]);
116
- });
42
+ },
43
+ ([text, ms]) => text,
44
+ );
45
+ textResourceShouldFail = false;
46
+}
47
+
48
+[true, false].forEach(deferPassiveEffectCleanupDuringUnmount => {
49
+ [true, false].forEach(runAllPassiveEffectDestroysBeforeCreates => {
50
+ describe(`ReactSuspenseWithNoopRenderer deferPassiveEffectCleanupDuringUnmount:${deferPassiveEffectCleanupDuringUnmount} runAllPassiveEffectDestroysBeforeCreates:${runAllPassiveEffectDestroysBeforeCreates}`, () => {
51
+ if (!__EXPERIMENTAL__) {
52
+ it("empty test so Jest doesn't complain", () => {});
53
+ return;
54
+ }
55
118
- it('does not restart rendering for initial render', async () => {
119
- function Bar(props) {
120
- Scheduler.unstable_yieldValue('Bar');
121
- return props.children;
122
- }
56
+ beforeEach(() => {
57
+ jest.resetModules();
58
124
- function Foo() {
125
- Scheduler.unstable_yieldValue('Foo');
126
- return (
127
- <>
128
- <Suspense fallback={<Text text="Loading..." />}>
129
- <Bar>
130
- <AsyncText text="A" ms={100} />
131
- <Text text="B" />
132
- </Bar>
133
- </Suspense>
134
- <Text text="C" />
135
- <Text text="D" />
136
- </>
137
- );
138
- }
139
-
140
- ReactNoop.render(<Foo />);
141
- expect(Scheduler).toFlushAndYieldThrough([
142
- 'Foo',
143
- 'Bar',
144
- // A suspends
145
- 'Suspend! [A]',
146
- // But we keep rendering the siblings
147
- 'B',
148
- 'Loading...',
149
- 'C',
150
- // We leave D incomplete.
151
- ]);
152
- expect(ReactNoop.getChildren()).toEqual([]);
153
-
154
- // Flush the promise completely
155
- Scheduler.unstable_advanceTime(100);
156
- await advanceTimers(100);
157
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
158
-
159
- // Even though the promise has resolved, we should now flush
160
- // and commit the in progress render instead of restarting.
161
- expect(Scheduler).toFlushAndYield(['D']);
162
- expect(ReactNoop.getChildren()).toEqual([
163
- span('Loading...'),
164
- span('C'),
165
- span('D'),
166
- ]);
167
-
168
- // Await one micro task to attach the retry listeners.
169
- await null;
170
-
171
- // Next, we'll flush the complete content.
172
- expect(Scheduler).toFlushAndYield(['Bar', 'A', 'B']);
173
-
174
- expect(ReactNoop.getChildren()).toEqual([
175
- span('A'),
176
- span('B'),
177
- span('C'),
178
- span('D'),
179
- ]);
180
- });
59
+ loadModules({
60
+ deferPassiveEffectCleanupDuringUnmount,
61
+ runAllPassiveEffectDestroysBeforeCreates,
62
+ });
63
+ });
64
182
- it('suspends rendering and continues later', async () => {
183
- function Bar(props) {
184
- Scheduler.unstable_yieldValue('Bar');
185
- return props.children;
186
- }
187
-
188
- function Foo({renderBar}) {
189
- Scheduler.unstable_yieldValue('Foo');
190
- return (
191
- <Suspense fallback={<Text text="Loading..." />}>
192
- {renderBar ? (
193
- <Bar>
194
- <AsyncText text="A" ms={100} />
195
- <Text text="B" />
196
- </Bar>
197
- ) : null}
198
- </Suspense>
199
- );
200
- }
201
-
202
- // Render empty shell.
203
- ReactNoop.render(<Foo />);
204
- expect(Scheduler).toFlushAndYield(['Foo']);
205
-
206
- // The update will suspend.
207
- ReactNoop.render(<Foo renderBar={true} />);
208
- expect(Scheduler).toFlushAndYield([
209
- 'Foo',
210
- 'Bar',
211
- // A suspends
212
- 'Suspend! [A]',
213
- // But we keep rendering the siblings
214
- 'B',
215
- 'Loading...',
216
- ]);
217
- expect(ReactNoop.getChildren()).toEqual([]);
218
-
219
- // Flush some of the time
220
- await advanceTimers(50);
221
- // Still nothing...
222
- expect(Scheduler).toFlushWithoutYielding();
223
- expect(ReactNoop.getChildren()).toEqual([]);
224
-
225
- // Flush the promise completely
226
- await advanceTimers(50);
227
- // Renders successfully
228
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
229
- expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'A', 'B']);
230
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
231
- });
65
+ // function div(...children) {
66
+ // children = children.map(
67
+ // c => (typeof c === 'string' ? {text: c, hidden: false} : c),
68
+ // );
69
+ // return {type: 'div', children, prop: undefined, hidden: false};
70
+ // }
71
233
- it('suspends siblings and later recovers each independently', async () => {
234
- // Render two sibling Suspense components
235
- ReactNoop.render(
236
- <Fragment>
237
- <Suspense fallback={<Text text="Loading A..." />}>
238
- <AsyncText text="A" ms={5000} />
239
- </Suspense>
240
- <Suspense fallback={<Text text="Loading B..." />}>
241
- <AsyncText text="B" ms={6000} />
242
- </Suspense>
243
- </Fragment>,
244
- );
245
- expect(Scheduler).toFlushAndYield([
246
- 'Suspend! [A]',
247
- 'Loading A...',
248
- 'Suspend! [B]',
249
- 'Loading B...',
250
- ]);
251
- expect(ReactNoop.getChildren()).toEqual([
252
- span('Loading A...'),
253
- span('Loading B...'),
254
- ]);
255
-
256
- // Advance time by enough that the first Suspense's promise resolves and
257
- // switches back to the normal view. The second Suspense should still
258
- // show the placeholder
259
- ReactNoop.expire(5000);
260
- await advanceTimers(5000);
261
-
262
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
263
- expect(Scheduler).toFlushAndYield(['A']);
264
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
265
-
266
- // Advance time by enough that the second Suspense's promise resolves
267
- // and switches back to the normal view
268
- ReactNoop.expire(1000);
269
- await advanceTimers(1000);
270
-
271
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
272
- expect(Scheduler).toFlushAndYield(['B']);
273
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
274
- });
72
+ function span(prop) {
73
+ return {type: 'span', children: [], prop, hidden: false};
74
+ }
75
276
- it('continues rendering siblings after suspending', async () => {
277
- // A shell is needed. The update cause it to suspend.
278
- ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
279
- expect(Scheduler).toFlushAndYield([]);
280
- // B suspends. Continue rendering the remaining siblings.
281
- ReactNoop.render(
282
- <Suspense fallback={<Text text="Loading..." />}>
283
- <Text text="A" />
284
- <AsyncText text="B" />
285
- <Text text="C" />
286
- <Text text="D" />
287
- </Suspense>,
288
- );
289
- // B suspends. Continue rendering the remaining siblings.
290
- expect(Scheduler).toFlushAndYield([
291
- 'A',
292
- 'Suspend! [B]',
293
- 'C',
294
- 'D',
295
- 'Loading...',
296
- ]);
297
- // Did not commit yet.
298
- expect(ReactNoop.getChildren()).toEqual([]);
299
-
300
- // Wait for data to resolve
301
- await advanceTimers(100);
302
- // Renders successfully
303
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
304
- expect(Scheduler).toFlushAndYield(['A', 'B', 'C', 'D']);
305
- expect(ReactNoop.getChildren()).toEqual([
306
- span('A'),
307
- span('B'),
308
- span('C'),
309
- span('D'),
310
- ]);
311
- });
76
+ function hiddenSpan(prop) {
77
+ return {type: 'span', children: [], prop, hidden: true};
78
+ }
79
313
- it('retries on error', async () => {
314
- class ErrorBoundary extends React.Component {
315
- state = {error: null};
316
- componentDidCatch(error) {
317
- this.setState({error});
80
+ function advanceTimers(ms) {
81
+ // Note: This advances Jest's virtual time but not React's. Use
82
+ // ReactNoop.expire for that.
83
+ if (typeof ms !== 'number') {
84
+ throw new Error('Must specify ms');
85
+ }
86
+ jest.advanceTimersByTime(ms);
87
+ // Wait until the end of the current tick
88
+ // We cannot use a timer since we're faking them
89
+ return Promise.resolve().then(() => {});
90
}
319
- reset() {
320
- this.setState({error: null});
91
+
92
+ function Text(props) {
93
+ Scheduler.unstable_yieldValue(props.text);
94
+ return <span prop={props.text} ref={props.hostRef} />;
95
}
322
- render() {
323
- if (this.state.error !== null) {
324
- return <Text text={'Caught error: ' + this.state.error.message} />;
96
+
97
+ function AsyncText(props) {
98
+ const text = props.text;
99
+ try {
100
+ TextResource.read([props.text, props.ms]);
101
+ Scheduler.unstable_yieldValue(text);
102
+ return <span prop={text} />;
103
+ } catch (promise) {
104
+ if (typeof promise.then === 'function') {
105
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
106
+ } else {
107
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
108
+ }
109
+ throw promise;
110
}
326
- return this.props.children;
111
}
328
- }
329
-
330
- const errorBoundary = React.createRef();
331
- function App({renderContent}) {
332
- return (
333
- <Suspense fallback={<Text text="Loading..." />}>
334
- {renderContent ? (
335
- <ErrorBoundary ref={errorBoundary}>
336
- <AsyncText text="Result" ms={1000} />
337
- </ErrorBoundary>
338
- ) : null}
339
- </Suspense>
340
- );
341
- }
112
343
- ReactNoop.render(<App />);
344
- expect(Scheduler).toFlushAndYield([]);
345
- expect(ReactNoop.getChildren()).toEqual([]);
113
+ it('warns if the deprecated maxDuration option is used', () => {
114
+ function Foo() {
115
+ return (
116
+ <Suspense maxDuration={100} fallback="Loading...">
117
+ <div />;
118
+ </Suspense>
119
+ );
120
+ }
121
+
122
+ ReactNoop.render(<Foo />);
123
+
124
+ expect(() => Scheduler.unstable_flushAll()).toErrorDev([
125
+ 'Warning: maxDuration has been removed from React. ' +
126
+ 'Remove the maxDuration prop.' +
127
+ '\n in Suspense (at **)' +
128
+ '\n in Foo (at **)',
129
+ ]);
130
+ });
131
+
132
+ it('does not restart rendering for initial render', async () => {
133
+ function Bar(props) {
134
+ Scheduler.unstable_yieldValue('Bar');
135
+ return props.children;
136
+ }
137
+
138
+ function Foo() {
139
+ Scheduler.unstable_yieldValue('Foo');
140
+ return (
141
+ <>
142
+ <Suspense fallback={<Text text="Loading..." />}>
143
+ <Bar>
144
+ <AsyncText text="A" ms={100} />
145
+ <Text text="B" />
146
+ </Bar>
147
+ </Suspense>
148
+ <Text text="C" />
149
+ <Text text="D" />
150
+ </>
151
+ );
152
+ }
153
347
- ReactNoop.render(<App renderContent={true} />);
348
- expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
349
- expect(ReactNoop.getChildren()).toEqual([]);
154
+ ReactNoop.render(<Foo />);
155
+ expect(Scheduler).toFlushAndYieldThrough([
156
+ 'Foo',
157
+ 'Bar',
158
+ // A suspends
159
+ 'Suspend! [A]',
160
+ // But we keep rendering the siblings
161
+ 'B',
162
+ 'Loading...',
163
+ 'C',
164
+ // We leave D incomplete.
165
+ ]);
166
+ expect(ReactNoop.getChildren()).toEqual([]);
167
351
- textResourceShouldFail = true;
352
- ReactNoop.expire(1000);
353
- await advanceTimers(1000);
354
- textResourceShouldFail = false;
168
+ // Flush the promise completely
169
+ Scheduler.unstable_advanceTime(100);
170
+ await advanceTimers(100);
171
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
172
356
- expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
173
+ // Even though the promise has resolved, we should now flush
174
+ // and commit the in progress render instead of restarting.
175
+ expect(Scheduler).toFlushAndYield(['D']);
176
+ expect(ReactNoop.getChildren()).toEqual([
177
+ span('Loading...'),
178
+ span('C'),
179
+ span('D'),
180
+ ]);
181
358
- expect(Scheduler).toFlushAndYield([
359
- 'Error! [Result]',
182
+ // Await one micro task to attach the retry listeners.
183
+ await null;
184
361
- // React retries one more time
362
- 'Error! [Result]',
185
+ // Next, we'll flush the complete content.
186
+ expect(Scheduler).toFlushAndYield(['Bar', 'A', 'B']);
187
364
- // Errored again on retry. Now handle it.
365
- 'Caught error: Failed to load: Result',
366
- ]);
367
- expect(ReactNoop.getChildren()).toEqual([
368
- span('Caught error: Failed to load: Result'),
369
- ]);
370
- });
188
+ expect(ReactNoop.getChildren()).toEqual([
189
+ span('A'),
190
+ span('B'),
191
+ span('C'),
192
+ span('D'),
193
+ ]);
194
+ });
195
372
- it('retries on error after falling back to a placeholder', async () => {
373
- class ErrorBoundary extends React.Component {
374
- state = {error: null};
375
- componentDidCatch(error) {
376
- this.setState({error});
377
- }
378
- reset() {
379
- this.setState({error: null});
380
- }
381
- render() {
382
- if (this.state.error !== null) {
383
- return <Text text={'Caught error: ' + this.state.error.message} />;
196
+ it('suspends rendering and continues later', async () => {
197
+ function Bar(props) {
198
+ Scheduler.unstable_yieldValue('Bar');
199
+ return props.children;
200
}
385
- return this.props.children;
386
- }
387
- }
388
-
389
- const errorBoundary = React.createRef();
390
- function App() {
391
- return (
392
- <Suspense fallback={<Text text="Loading..." />}>
393
- <ErrorBoundary ref={errorBoundary}>
394
- <AsyncText text="Result" ms={3000} />
395
- </ErrorBoundary>
396
- </Suspense>
397
- );
398
- }
201
400
- ReactNoop.render(<App />);
401
- expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
402
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
202
+ function Foo({renderBar}) {
203
+ Scheduler.unstable_yieldValue('Foo');
204
+ return (
205
+ <Suspense fallback={<Text text="Loading..." />}>
206
+ {renderBar ? (
207
+ <Bar>
208
+ <AsyncText text="A" ms={100} />
209
+ <Text text="B" />
210
+ </Bar>
211
+ ) : null}
212
+ </Suspense>
213
+ );
214
+ }
215
404
- textResourceShouldFail = true;
405
- ReactNoop.expire(3000);
406
- await advanceTimers(3000);
407
- textResourceShouldFail = false;
216
+ // Render empty shell.
217
+ ReactNoop.render(<Foo />);
218
+ expect(Scheduler).toFlushAndYield(['Foo']);
219
+
220
+ // The update will suspend.
221
+ ReactNoop.render(<Foo renderBar={true} />);
222
+ expect(Scheduler).toFlushAndYield([
223
+ 'Foo',
224
+ 'Bar',
225
+ // A suspends
226
+ 'Suspend! [A]',
227
+ // But we keep rendering the siblings
228
+ 'B',
229
+ 'Loading...',
230
+ ]);
231
+ expect(ReactNoop.getChildren()).toEqual([]);
232
+
233
+ // Flush some of the time
234
+ await advanceTimers(50);
235
+ // Still nothing...
236
+ expect(Scheduler).toFlushWithoutYielding();
237
+ expect(ReactNoop.getChildren()).toEqual([]);
238
+
239
+ // Flush the promise completely
240
+ await advanceTimers(50);
241
+ // Renders successfully
242
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
243
+ expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'A', 'B']);
244
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
245
+ });
246
409
- expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
410
- expect(Scheduler).toFlushAndYield([
411
- 'Error! [Result]',
247
+ it('suspends siblings and later recovers each independently', async () => {
248
+ // Render two sibling Suspense components
249
+ ReactNoop.render(
250
+ <Fragment>
251
+ <Suspense fallback={<Text text="Loading A..." />}>
252
+ <AsyncText text="A" ms={5000} />
253
+ </Suspense>
254
+ <Suspense fallback={<Text text="Loading B..." />}>
255
+ <AsyncText text="B" ms={6000} />
256
+ </Suspense>
257
+ </Fragment>,
258
+ );
259
+ expect(Scheduler).toFlushAndYield([
260
+ 'Suspend! [A]',
261
+ 'Loading A...',
262
+ 'Suspend! [B]',
263
+ 'Loading B...',
264
+ ]);
265
+ expect(ReactNoop.getChildren()).toEqual([
266
+ span('Loading A...'),
267
+ span('Loading B...'),
268
+ ]);
269
413
- // React retries one more time
414
- 'Error! [Result]',
270
+ // Advance time by enough that the first Suspense's promise resolves and
271
+ // switches back to the normal view. The second Suspense should still
272
+ // show the placeholder
273
+ ReactNoop.expire(5000);
274
+ await advanceTimers(5000);
275
416
- // Errored again on retry. Now handle it.
276
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
277
+ expect(Scheduler).toFlushAndYield(['A']);
278
+ expect(ReactNoop.getChildren()).toEqual([
279
+ span('A'),
280
+ span('Loading B...'),
281
+ ]);
282
418
- 'Caught error: Failed to load: Result',
419
- ]);
420
- expect(ReactNoop.getChildren()).toEqual([
421
- span('Caught error: Failed to load: Result'),
422
- ]);
423
- });
283
+ // Advance time by enough that the second Suspense's promise resolves
284
+ // and switches back to the normal view
285
+ ReactNoop.expire(1000);
286
+ await advanceTimers(1000);
287
425
- it('can update at a higher priority while in a suspended state', async () => {
426
- function App(props) {
427
- return (
428
- <Suspense fallback={<Text text="Loading..." />}>
429
- <Text text={props.highPri} />
430
- <AsyncText text={props.lowPri} />
431
- </Suspense>
432
- );
433
- }
434
-
435
- // Initial mount
436
- ReactNoop.render(<App highPri="A" lowPri="1" />);
437
- expect(Scheduler).toFlushAndYield(['A', 'Suspend! [1]', 'Loading...']);
438
- await advanceTimers(0);
439
- expect(Scheduler).toHaveYielded(['Promise resolved [1]']);
440
- expect(Scheduler).toFlushAndYield(['A', '1']);
441
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('1')]);
442
-
443
- // Update the low-pri text
444
- ReactNoop.render(<App highPri="A" lowPri="2" />);
445
- expect(Scheduler).toFlushAndYield([
446
- 'A',
447
- // Suspends
448
- 'Suspend! [2]',
449
- 'Loading...',
450
- ]);
451
-
452
- // While we're still waiting for the low-pri update to complete, update the
453
- // high-pri text at high priority.
454
- ReactNoop.flushSync(() => {
455
- ReactNoop.render(<App highPri="B" lowPri="1" />);
456
- });
457
- expect(Scheduler).toHaveYielded(['B', '1']);
458
- expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
288
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
289
+ expect(Scheduler).toFlushAndYield(['B']);
290
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
291
+ });
292
460
- // Unblock the low-pri text and finish
461
- await advanceTimers(0);
462
- expect(Scheduler).toHaveYielded(['Promise resolved [2]']);
463
- expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
464
- });
293
+ it('continues rendering siblings after suspending', async () => {
294
+ // A shell is needed. The update cause it to suspend.
295
+ ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
296
+ expect(Scheduler).toFlushAndYield([]);
297
+ // B suspends. Continue rendering the remaining siblings.
298
+ ReactNoop.render(
299
+ <Suspense fallback={<Text text="Loading..." />}>
300
+ <Text text="A" />
301
+ <AsyncText text="B" />
302
+ <Text text="C" />
303
+ <Text text="D" />
304
+ </Suspense>,
305
+ );
306
+ // B suspends. Continue rendering the remaining siblings.
307
+ expect(Scheduler).toFlushAndYield([
308
+ 'A',
309
+ 'Suspend! [B]',
310
+ 'C',
311
+ 'D',
312
+ 'Loading...',
313
+ ]);
314
+ // Did not commit yet.
315
+ expect(ReactNoop.getChildren()).toEqual([]);
316
+
317
+ // Wait for data to resolve
318
+ await advanceTimers(100);
319
+ // Renders successfully
320
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
321
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'C', 'D']);
322
+ expect(ReactNoop.getChildren()).toEqual([
323
+ span('A'),
324
+ span('B'),
325
+ span('C'),
326
+ span('D'),
327
+ ]);
328
+ });
329
466
- it('keeps working on lower priority work after being pinged', async () => {
467
- // Advance the virtual time so that we're close to the edge of a bucket.
468
- ReactNoop.expire(149);
330
+ it('retries on error', async () => {
331
+ class ErrorBoundary extends React.Component {
332
+ state = {error: null};
333
+ componentDidCatch(error) {
334
+ this.setState({error});
335
+ }
336
+ reset() {
337
+ this.setState({error: null});
338
+ }
339
+ render() {
340
+ if (this.state.error !== null) {
341
+ return (
342
+ <Text text={'Caught error: ' + this.state.error.message} />
343
+ );
344
+ }
345
+ return this.props.children;
346
+ }
347
+ }
348
470
- function App(props) {
471
- return (
472
- <Suspense fallback={<Text text="Loading..." />}>
473
- {props.showA && <AsyncText text="A" />}
474
- {props.showB && <Text text="B" />}
475
- </Suspense>
476
- );
477
- }
478
-
479
- ReactNoop.render(<App showA={false} showB={false} />);
480
- expect(Scheduler).toFlushAndYield([]);
481
- expect(ReactNoop.getChildren()).toEqual([]);
482
-
483
- ReactNoop.render(<App showA={true} showB={false} />);
484
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
485
- expect(ReactNoop.getChildren()).toEqual([]);
486
-
487
- // Advance React's virtual time by enough to fall into a new async bucket,
488
- // but not enough to expire the suspense timeout.
489
- ReactNoop.expire(120);
490
- ReactNoop.render(<App showA={true} showB={true} />);
491
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'B', 'Loading...']);
492
- expect(ReactNoop.getChildren()).toEqual([]);
493
-
494
- await advanceTimers(0);
495
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
496
- expect(Scheduler).toFlushAndYield(['A', 'B']);
497
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
498
- });
349
+ const errorBoundary = React.createRef();
350
+ function App({renderContent}) {
351
+ return (
352
+ <Suspense fallback={<Text text="Loading..." />}>
353
+ {renderContent ? (
354
+ <ErrorBoundary ref={errorBoundary}>
355
+ <AsyncText text="Result" ms={1000} />
356
+ </ErrorBoundary>
357
+ ) : null}
358
+ </Suspense>
359
+ );
360
+ }
361
500
- it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => {
501
- function App(props) {
502
- if (props.hide) {
503
- return <Text text="(empty)" />;
504
- }
505
- return (
506
- <Suspense fallback="Loading...">
507
- <AsyncText ms={2000} text="Async" />
508
- </Suspense>
509
- );
510
- }
362
+ ReactNoop.render(<App />);
363
+ expect(Scheduler).toFlushAndYield([]);
364
+ expect(ReactNoop.getChildren()).toEqual([]);
365
512
- // Schedule a high pri update and a low pri update, without rendering in
513
- // between.
514
- ReactNoop.discreteUpdates(() => {
515
- // High pri
516
- ReactNoop.render(<App />);
517
- });
518
- // Low pri
519
- ReactNoop.render(<App hide={true} />);
520
-
521
- expect(Scheduler).toFlushAndYield([
522
- // The first update suspends
523
- 'Suspend! [Async]',
524
- // but we have another pending update that we can work on
525
- '(empty)',
526
- ]);
527
- expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
528
- });
366
+ ReactNoop.render(<App renderContent={true} />);
367
+ expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
368
+ expect(ReactNoop.getChildren()).toEqual([]);
369
530
- it('tries each subsequent level after suspending', async () => {
531
- const root = ReactNoop.createRoot();
532
-
533
- function App({step, shouldSuspend}) {
534
- return (
535
- <Suspense fallback="Loading...">
536
- <Text text="Sibling" />
537
- {shouldSuspend ? (
538
- <AsyncText ms={10000} text={'Step ' + step} />
539
- ) : (
540
- <Text text={'Step ' + step} />
541
- )}
542
- </Suspense>
543
- );
544
- }
370
+ textResourceShouldFail = true;
371
+ ReactNoop.expire(1000);
372
+ await advanceTimers(1000);
373
+ textResourceShouldFail = false;
374
+
375
+ expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
376
+
377
+ expect(Scheduler).toFlushAndYield([
378
+ 'Error! [Result]',
379
546
- function interrupt() {
547
- // React has a heuristic to batch all updates that occur within the same
548
- // event. This is a trick to circumvent that heuristic.
549
- ReactNoop.flushSync(() => {
550
- ReactNoop.renderToRootWithID(null, 'other-root');
380
+ // React retries one more time
381
+ 'Error! [Result]',
382
+
383
+ // Errored again on retry. Now handle it.
384
+ 'Caught error: Failed to load: Result',
385
+ ]);
386
+ expect(ReactNoop.getChildren()).toEqual([
387
+ span('Caught error: Failed to load: Result'),
388
+ ]);
389
});
552
- }
390
554
- // Mount the Suspense boundary without suspending, so that the subsequent
555
- // updates suspend with a delay.
556
- await ReactNoop.act(async () => {
557
- root.render(<App step={0} shouldSuspend={false} />);
558
- });
559
- await advanceTimers(1000);
560
- expect(Scheduler).toHaveYielded(['Sibling', 'Step 0']);
561
-
562
- // Schedule an update at several distinct expiration times
563
- await ReactNoop.act(async () => {
564
- root.render(<App step={1} shouldSuspend={true} />);
565
- Scheduler.unstable_advanceTime(1000);
566
- expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
567
- interrupt();
568
-
569
- root.render(<App step={2} shouldSuspend={true} />);
570
- Scheduler.unstable_advanceTime(1000);
571
- expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
572
- interrupt();
573
-
574
- root.render(<App step={3} shouldSuspend={true} />);
575
- Scheduler.unstable_advanceTime(1000);
576
- expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
577
- interrupt();
578
-
579
- root.render(<App step={4} shouldSuspend={false} />);
580
- });
391
+ it('retries on error after falling back to a placeholder', async () => {
392
+ class ErrorBoundary extends React.Component {
393
+ state = {error: null};
394
+ componentDidCatch(error) {
395
+ this.setState({error});
396
+ }
397
+ reset() {
398
+ this.setState({error: null});
399
+ }
400
+ render() {
401
+ if (this.state.error !== null) {
402
+ return (
403
+ <Text text={'Caught error: ' + this.state.error.message} />
404
+ );
405
+ }
406
+ return this.props.children;
407
+ }
408
+ }
409
582
- // Should suspend at each distinct level
583
- expect(Scheduler).toHaveYielded([
584
- 'Sibling',
585
- 'Suspend! [Step 1]',
586
- 'Sibling',
587
- 'Suspend! [Step 2]',
588
- 'Sibling',
589
- 'Suspend! [Step 3]',
590
- 'Sibling',
591
- 'Step 4',
592
- ]);
593
- });
410
+ const errorBoundary = React.createRef();
411
+ function App() {
412
+ return (
413
+ <Suspense fallback={<Text text="Loading..." />}>
414
+ <ErrorBoundary ref={errorBoundary}>
415
+ <AsyncText text="Result" ms={3000} />
416
+ </ErrorBoundary>
417
+ </Suspense>
418
+ );
419
+ }
420
595
- it('forces an expiration after an update times out', async () => {
596
- ReactNoop.render(
597
- <Fragment>
598
- <Suspense fallback={<Text text="Loading..." />} />
599
- </Fragment>,
600
- );
601
- expect(Scheduler).toFlushAndYield([]);
602
-
603
- ReactNoop.render(
604
- <Fragment>
605
- <Suspense fallback={<Text text="Loading..." />}>
606
- <AsyncText text="Async" ms={20000} />
607
- </Suspense>
608
- <Text text="Sync" />
609
- </Fragment>,
610
- );
611
-
612
- expect(Scheduler).toFlushAndYield([
613
- // The async child suspends
614
- 'Suspend! [Async]',
615
- // Render the placeholder
616
- 'Loading...',
617
- // Continue on the sibling
618
- 'Sync',
619
- ]);
620
- // The update hasn't expired yet, so we commit nothing.
621
- expect(ReactNoop.getChildren()).toEqual([]);
622
-
623
- // Advance both React's virtual time and Jest's timers by enough to expire
624
- // the update, but not by enough to flush the suspending promise.
625
- ReactNoop.expire(10000);
626
- await advanceTimers(10000);
627
- // No additional rendering work is required, since we already prepared
628
- // the placeholder.
629
- expect(Scheduler).toHaveYielded([]);
630
- // Should have committed the placeholder.
631
- expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
632
-
633
- // Once the promise resolves, we render the suspended view
634
- await advanceTimers(10000);
635
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
636
- expect(Scheduler).toFlushAndYield(['Async']);
637
- expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
638
- });
421
+ ReactNoop.render(<App />);
422
+ expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
423
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
424
640
- it('switches to an inner fallback after suspending for a while', async () => {
641
- // Advance the virtual time so that we're closer to the edge of a bucket.
642
- ReactNoop.expire(200);
643
-
644
- ReactNoop.render(
645
- <Fragment>
646
- <Text text="Sync" />
647
- <Suspense fallback={<Text text="Loading outer..." />}>
648
- <AsyncText text="Outer content" ms={300} />
649
- <Suspense fallback={<Text text="Loading inner..." />}>
650
- <AsyncText text="Inner content" ms={1000} />
651
- </Suspense>
652
- </Suspense>
653
- </Fragment>,
654
- );
655
-
656
- expect(Scheduler).toFlushAndYield([
657
- 'Sync',
658
- // The async content suspends
659
- 'Suspend! [Outer content]',
660
- 'Suspend! [Inner content]',
661
- 'Loading inner...',
662
- 'Loading outer...',
663
- ]);
664
- // The outer loading state finishes immediately.
665
- expect(ReactNoop.getChildren()).toEqual([
666
- span('Sync'),
667
- span('Loading outer...'),
668
- ]);
669
-
670
- // Resolve the outer promise.
671
- ReactNoop.expire(300);
672
- await advanceTimers(300);
673
- expect(Scheduler).toHaveYielded(['Promise resolved [Outer content]']);
674
- expect(Scheduler).toFlushAndYield([
675
- 'Outer content',
676
- 'Suspend! [Inner content]',
677
- 'Loading inner...',
678
- ]);
679
- // Don't commit the inner placeholder yet.
680
- expect(ReactNoop.getChildren()).toEqual([
681
- span('Sync'),
682
- span('Loading outer...'),
683
- ]);
684
-
685
- // Expire the inner timeout.
686
- ReactNoop.expire(500);
687
- await advanceTimers(500);
688
- // Now that 750ms have elapsed since the outer placeholder timed out,
689
- // we can timeout the inner placeholder.
690
- expect(ReactNoop.getChildren()).toEqual([
691
- span('Sync'),
692
- span('Outer content'),
693
- span('Loading inner...'),
694
- ]);
695
-
696
- // Finally, flush the inner promise. We should see the complete screen.
697
- ReactNoop.expire(1000);
698
- await advanceTimers(1000);
699
- expect(Scheduler).toHaveYielded(['Promise resolved [Inner content]']);
700
- expect(Scheduler).toFlushAndYield(['Inner content']);
701
- expect(ReactNoop.getChildren()).toEqual([
702
- span('Sync'),
703
- span('Outer content'),
704
- span('Inner content'),
705
- ]);
706
- });
425
+ textResourceShouldFail = true;
426
+ ReactNoop.expire(3000);
427
+ await advanceTimers(3000);
428
+ textResourceShouldFail = false;
429
708
- it('renders an expiration boundary synchronously', async () => {
709
- spyOnDev(console, 'error');
710
- // Synchronously render a tree that suspends
711
- ReactNoop.flushSync(() =>
712
- ReactNoop.render(
713
- <Fragment>
714
- <Suspense fallback={<Text text="Loading..." />}>
715
- <AsyncText text="Async" />
716
- </Suspense>
717
- <Text text="Sync" />
718
- </Fragment>,
719
- ),
720
- );
721
- expect(Scheduler).toHaveYielded([
722
- // The async child suspends
723
- 'Suspend! [Async]',
724
- // We immediately render the fallback UI
725
- 'Loading...',
726
- // Continue on the sibling
727
- 'Sync',
728
- ]);
729
- // The tree commits synchronously
730
- expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
731
-
732
- // Once the promise resolves, we render the suspended view
733
- await advanceTimers(0);
734
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
735
- expect(Scheduler).toFlushAndYield(['Async']);
736
- expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
737
- });
430
+ expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
431
+ expect(Scheduler).toFlushAndYield([
432
+ 'Error! [Result]',
433
+
434
+ // React retries one more time
435
+ 'Error! [Result]',
436
+
437
+ // Errored again on retry. Now handle it.
438
+
439
+ 'Caught error: Failed to load: Result',
440
+ ]);
441
+ expect(ReactNoop.getChildren()).toEqual([
442
+ span('Caught error: Failed to load: Result'),
443
+ ]);
444
+ });
445
739
- it('suspending inside an expired expiration boundary will bubble to the next one', async () => {
740
- ReactNoop.flushSync(() =>
741
- ReactNoop.render(
742
- <Fragment>
743
- <Suspense fallback={<Text text="Loading (outer)..." />}>
744
- <Suspense fallback={<AsyncText text="Loading (inner)..." />}>
745
- <AsyncText text="Async" />
446
+ it('can update at a higher priority while in a suspended state', async () => {
447
+ function App(props) {
448
+ return (
449
+ <Suspense fallback={<Text text="Loading..." />}>
450
+ <Text text={props.highPri} />
451
+ <AsyncText text={props.lowPri} />
452
+ </Suspense>
453
+ );
454
+ }
455
+
456
+ // Initial mount
457
+ ReactNoop.render(<App highPri="A" lowPri="1" />);
458
+ expect(Scheduler).toFlushAndYield(['A', 'Suspend! [1]', 'Loading...']);
459
+ await advanceTimers(0);
460
+ expect(Scheduler).toHaveYielded(['Promise resolved [1]']);
461
+ expect(Scheduler).toFlushAndYield(['A', '1']);
462
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('1')]);
463
+
464
+ // Update the low-pri text
465
+ ReactNoop.render(<App highPri="A" lowPri="2" />);
466
+ expect(Scheduler).toFlushAndYield([
467
+ 'A',
468
+ // Suspends
469
+ 'Suspend! [2]',
470
+ 'Loading...',
471
+ ]);
472
+
473
+ // While we're still waiting for the low-pri update to complete, update the
474
+ // high-pri text at high priority.
475
+ ReactNoop.flushSync(() => {
476
+ ReactNoop.render(<App highPri="B" lowPri="1" />);
477
+ });
478
+ expect(Scheduler).toHaveYielded(['B', '1']);
479
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
480
+
481
+ // Unblock the low-pri text and finish
482
+ await advanceTimers(0);
483
+ expect(Scheduler).toHaveYielded(['Promise resolved [2]']);
484
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
485
+ });
486
+
487
+ it('keeps working on lower priority work after being pinged', async () => {
488
+ // Advance the virtual time so that we're close to the edge of a bucket.
489
+ ReactNoop.expire(149);
490
+
491
+ function App(props) {
492
+ return (
493
+ <Suspense fallback={<Text text="Loading..." />}>
494
+ {props.showA && <AsyncText text="A" />}
495
+ {props.showB && <Text text="B" />}
496
+ </Suspense>
497
+ );
498
+ }
499
+
500
+ ReactNoop.render(<App showA={false} showB={false} />);
501
+ expect(Scheduler).toFlushAndYield([]);
502
+ expect(ReactNoop.getChildren()).toEqual([]);
503
+
504
+ ReactNoop.render(<App showA={true} showB={false} />);
505
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
506
+ expect(ReactNoop.getChildren()).toEqual([]);
507
+
508
+ // Advance React's virtual time by enough to fall into a new async bucket,
509
+ // but not enough to expire the suspense timeout.
510
+ ReactNoop.expire(120);
511
+ ReactNoop.render(<App showA={true} showB={true} />);
512
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'B', 'Loading...']);
513
+ expect(ReactNoop.getChildren()).toEqual([]);
514
+
515
+ await advanceTimers(0);
516
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
517
+ expect(Scheduler).toFlushAndYield(['A', 'B']);
518
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
519
+ });
520
+
521
+ it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => {
522
+ function App(props) {
523
+ if (props.hide) {
524
+ return <Text text="(empty)" />;
525
+ }
526
+ return (
527
+ <Suspense fallback="Loading...">
528
+ <AsyncText ms={2000} text="Async" />
529
+ </Suspense>
530
+ );
531
+ }
532
+
533
+ // Schedule a high pri update and a low pri update, without rendering in
534
+ // between.
535
+ ReactNoop.discreteUpdates(() => {
536
+ // High pri
537
+ ReactNoop.render(<App />);
538
+ });
539
+ // Low pri
540
+ ReactNoop.render(<App hide={true} />);
541
+
542
+ expect(Scheduler).toFlushAndYield([
543
+ // The first update suspends
544
+ 'Suspend! [Async]',
545
+ // but we have another pending update that we can work on
546
+ '(empty)',
547
+ ]);
548
+ expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
549
+ });
550
+
551
+ it('tries each subsequent level after suspending', async () => {
552
+ const root = ReactNoop.createRoot();
553
+
554
+ function App({step, shouldSuspend}) {
555
+ return (
556
+ <Suspense fallback="Loading...">
557
+ <Text text="Sibling" />
558
+ {shouldSuspend ? (
559
+ <AsyncText ms={10000} text={'Step ' + step} />
560
+ ) : (
561
+ <Text text={'Step ' + step} />
562
+ )}
563
+ </Suspense>
564
+ );
565
+ }
566
+
567
+ function interrupt() {
568
+ // React has a heuristic to batch all updates that occur within the same
569
+ // event. This is a trick to circumvent that heuristic.
570
+ ReactNoop.flushSync(() => {
571
+ ReactNoop.renderToRootWithID(null, 'other-root');
572
+ });
573
+ }
574
+
575
+ // Mount the Suspense boundary without suspending, so that the subsequent
576
+ // updates suspend with a delay.
577
+ await ReactNoop.act(async () => {
578
+ root.render(<App step={0} shouldSuspend={false} />);
579
+ });
580
+ await advanceTimers(1000);
581
+ expect(Scheduler).toHaveYielded(['Sibling', 'Step 0']);
582
+
583
+ // Schedule an update at several distinct expiration times
584
+ await ReactNoop.act(async () => {
585
+ root.render(<App step={1} shouldSuspend={true} />);
586
+ Scheduler.unstable_advanceTime(1000);
587
+ expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
588
+ interrupt();
589
+
590
+ root.render(<App step={2} shouldSuspend={true} />);
591
+ Scheduler.unstable_advanceTime(1000);
592
+ expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
593
+ interrupt();
594
+
595
+ root.render(<App step={3} shouldSuspend={true} />);
596
+ Scheduler.unstable_advanceTime(1000);
597
+ expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
598
+ interrupt();
599
+
600
+ root.render(<App step={4} shouldSuspend={false} />);
601
+ });
602
+
603
+ // Should suspend at each distinct level
604
+ expect(Scheduler).toHaveYielded([
605
+ 'Sibling',
606
+ 'Suspend! [Step 1]',
607
+ 'Sibling',
608
+ 'Suspend! [Step 2]',
609
+ 'Sibling',
610
+ 'Suspend! [Step 3]',
611
+ 'Sibling',
612
+ 'Step 4',
613
+ ]);
614
+ });
615
+
616
+ it('forces an expiration after an update times out', async () => {
617
+ ReactNoop.render(
618
+ <Fragment>
619
+ <Suspense fallback={<Text text="Loading..." />} />
620
+ </Fragment>,
621
+ );
622
+ expect(Scheduler).toFlushAndYield([]);
623
+
624
+ ReactNoop.render(
625
+ <Fragment>
626
+ <Suspense fallback={<Text text="Loading..." />}>
627
+ <AsyncText text="Async" ms={20000} />
628
</Suspense>
629
<Text text="Sync" />
748
- </Suspense>
749
- </Fragment>,
750
- ),
751
- );
752
- expect(Scheduler).toHaveYielded([
753
- 'Suspend! [Async]',
754
- 'Suspend! [Loading (inner)...]',
755
- 'Sync',
756
- 'Loading (outer)...',
757
- ]);
758
- // The tree commits synchronously
759
- expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]);
760
- });
630
+ </Fragment>,
631
+ );
632
762
- it('expires early by default', async () => {
763
- ReactNoop.render(
764
- <Fragment>
765
- <Suspense fallback={<Text text="Loading..." />} />
766
- </Fragment>,
767
- );
768
- expect(Scheduler).toFlushAndYield([]);
769
-
770
- ReactNoop.render(
771
- <Fragment>
772
- <Suspense fallback={<Text text="Loading..." />}>
773
- <AsyncText text="Async" ms={3000} />
774
- </Suspense>
775
- <Text text="Sync" />
776
- </Fragment>,
777
- );
778
-
779
- expect(Scheduler).toFlushAndYield([
780
- // The async child suspends
781
- 'Suspend! [Async]',
782
- 'Loading...',
783
- // Continue on the sibling
784
- 'Sync',
785
- ]);
786
- // The update hasn't expired yet, so we commit nothing.
787
- expect(ReactNoop.getChildren()).toEqual([]);
788
-
789
- // Advance both React's virtual time and Jest's timers by enough to trigger
790
- // the timeout, but not by enough to flush the promise or reach the true
791
- // expiration time.
792
- ReactNoop.expire(2000);
793
- await advanceTimers(2000);
794
- expect(Scheduler).toFlushWithoutYielding();
795
- expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
796
-
797
- // Once the promise resolves, we render the suspended view
798
- await advanceTimers(1000);
799
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
800
- expect(Scheduler).toFlushAndYield(['Async']);
801
- expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
802
- });
633
+ expect(Scheduler).toFlushAndYield([
634
+ // The async child suspends
635
+ 'Suspend! [Async]',
636
+ // Render the placeholder
637
+ 'Loading...',
638
+ // Continue on the sibling
639
+ 'Sync',
640
+ ]);
641
+ // The update hasn't expired yet, so we commit nothing.
642
+ expect(ReactNoop.getChildren()).toEqual([]);
643
+
644
+ // Advance both React's virtual time and Jest's timers by enough to expire
645
+ // the update, but not by enough to flush the suspending promise.
646
+ ReactNoop.expire(10000);
647
+ await advanceTimers(10000);
648
+ // No additional rendering work is required, since we already prepared
649
+ // the placeholder.
650
+ expect(Scheduler).toHaveYielded([]);
651
+ // Should have committed the placeholder.
652
+ expect(ReactNoop.getChildren()).toEqual([
653
+ span('Loading...'),
654
+ span('Sync'),
655
+ ]);
656
804
- it('resolves successfully even if fallback render is pending', async () => {
805
- ReactNoop.render(
806
- <>
807
- <Suspense fallback={<Text text="Loading..." />} />
808
- </>,
809
- );
810
- expect(Scheduler).toFlushAndYield([]);
811
- expect(ReactNoop.getChildren()).toEqual([]);
812
- ReactNoop.render(
813
- <>
814
- <Suspense fallback={<Text text="Loading..." />}>
815
- <AsyncText text="Async" ms={3000} />
816
- </Suspense>
817
- </>,
818
- );
819
- expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
820
- await advanceTimers(1500);
821
- expect(Scheduler).toHaveYielded([]);
822
- expect(ReactNoop.getChildren()).toEqual([]);
823
- // Before we have a chance to flush, the promise resolves.
824
- await advanceTimers(2000);
825
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
826
- expect(Scheduler).toFlushAndYield([
827
- // We've now pinged the boundary but we don't know if we should restart yet,
828
- // because we haven't completed the suspense boundary.
829
- 'Loading...',
830
- // Once we've completed the boundary we restarted.
831
- 'Async',
832
- ]);
833
- expect(ReactNoop.getChildren()).toEqual([span('Async')]);
834
- });
657
+ // Once the promise resolves, we render the suspended view
658
+ await advanceTimers(10000);
659
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
660
+ expect(Scheduler).toFlushAndYield(['Async']);
661
+ expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
662
+ });
663
836
- it('throws a helpful error when an update is suspends without a placeholder', () => {
837
- ReactNoop.render(<AsyncText ms={1000} text="Async" />);
838
- expect(Scheduler).toFlushAndThrow(
839
- 'AsyncText suspended while rendering, but no fallback UI was specified.',
840
- );
841
- });
664
+ it('switches to an inner fallback after suspending for a while', async () => {
665
+ // Advance the virtual time so that we're closer to the edge of a bucket.
666
+ ReactNoop.expire(200);
667
843
- it('a Suspense component correctly handles more than one suspended child', async () => {
844
- ReactNoop.render(
845
- <Suspense fallback={<Text text="Loading..." />}>
846
- <AsyncText text="A" ms={100} />
847
- <AsyncText text="B" ms={100} />
848
- </Suspense>,
849
- );
850
- Scheduler.unstable_advanceTime(10000);
851
- expect(Scheduler).toFlushExpired([
852
- 'Suspend! [A]',
853
- 'Suspend! [B]',
854
- 'Loading...',
855
- ]);
856
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
857
-
858
- await advanceTimers(100);
859
-
860
- expect(Scheduler).toHaveYielded([
861
- 'Promise resolved [A]',
862
- 'Promise resolved [B]',
863
- ]);
864
- expect(Scheduler).toFlushAndYield(['A', 'B']);
865
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
866
- });
668
+ ReactNoop.render(
669
+ <Fragment>
670
+ <Text text="Sync" />
671
+ <Suspense fallback={<Text text="Loading outer..." />}>
672
+ <AsyncText text="Outer content" ms={300} />
673
+ <Suspense fallback={<Text text="Loading inner..." />}>
674
+ <AsyncText text="Inner content" ms={1000} />
675
+ </Suspense>
676
+ </Suspense>
677
+ </Fragment>,
678
+ );
679
868
- it('can resume rendering earlier than a timeout', async () => {
869
- ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
870
- expect(Scheduler).toFlushAndYield([]);
871
-
872
- ReactNoop.render(
873
- <Suspense fallback={<Text text="Loading..." />}>
874
- <AsyncText text="Async" ms={100} />
875
- </Suspense>,
876
- );
877
- expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
878
- expect(ReactNoop.getChildren()).toEqual([]);
879
-
880
- // Advance time by an amount slightly smaller than what's necessary to
881
- // resolve the promise
882
- await advanceTimers(99);
883
-
884
- // Nothing has rendered yet
885
- expect(Scheduler).toFlushWithoutYielding();
886
- expect(ReactNoop.getChildren()).toEqual([]);
887
-
888
- // Resolve the promise
889
- await advanceTimers(1);
890
- // We can now resume rendering
891
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
892
- expect(Scheduler).toFlushAndYield(['Async']);
893
- expect(ReactNoop.getChildren()).toEqual([span('Async')]);
894
- });
680
+ expect(Scheduler).toFlushAndYield([
681
+ 'Sync',
682
+ // The async content suspends
683
+ 'Suspend! [Outer content]',
684
+ 'Suspend! [Inner content]',
685
+ 'Loading inner...',
686
+ 'Loading outer...',
687
+ ]);
688
+ // The outer loading state finishes immediately.
689
+ expect(ReactNoop.getChildren()).toEqual([
690
+ span('Sync'),
691
+ span('Loading outer...'),
692
+ ]);
693
896
- it('starts working on an update even if its priority falls between two suspended levels', async () => {
897
- function App(props) {
898
- return (
899
- <Suspense fallback={<Text text="Loading..." />}>
900
- {props.text === 'C' || props.text === 'S' ? (
901
- <Text text={props.text} />
902
- ) : (
903
- <AsyncText text={props.text} ms={10000} />
904
- )}
905
- </Suspense>
906
- );
907
- }
908
-
909
- // First mount without suspending. This ensures we already have content
910
- // showing so that subsequent updates will suspend.
911
- ReactNoop.render(<App text="S" />);
912
- expect(Scheduler).toFlushAndYield(['S']);
913
-
914
- // Schedule an update, and suspend for up to 5 seconds.
915
- React.unstable_withSuspenseConfig(
916
- () => ReactNoop.render(<App text="A" />),
917
- {
918
- timeoutMs: 5000,
919
- },
920
- );
921
- // The update should suspend.
922
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
923
- expect(ReactNoop.getChildren()).toEqual([span('S')]);
924
-
925
- // Advance time until right before it expires.
926
- await advanceTimers(4999);
927
- ReactNoop.expire(4999);
928
- expect(Scheduler).toFlushWithoutYielding();
929
- expect(ReactNoop.getChildren()).toEqual([span('S')]);
930
-
931
- // Schedule another low priority update.
932
- React.unstable_withSuspenseConfig(
933
- () => ReactNoop.render(<App text="B" />),
934
- {
935
- timeoutMs: 10000,
936
- },
937
- );
938
- // This update should also suspend.
939
- expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
940
- expect(ReactNoop.getChildren()).toEqual([span('S')]);
941
-
942
- // Schedule a regular update. Its expiration time will fall between
943
- // the expiration times of the previous two updates.
944
- ReactNoop.render(<App text="C" />);
945
- expect(Scheduler).toFlushAndYield(['C']);
946
- expect(ReactNoop.getChildren()).toEqual([span('C')]);
947
-
948
- await advanceTimers(10000);
949
- // Flush the remaining work.
950
- expect(Scheduler).toHaveYielded([
951
- 'Promise resolved [A]',
952
- 'Promise resolved [B]',
953
- ]);
954
- // Nothing else to render.
955
- expect(Scheduler).toFlushWithoutYielding();
956
- expect(ReactNoop.getChildren()).toEqual([span('C')]);
957
- });
694
+ // Resolve the outer promise.
695
+ ReactNoop.expire(300);
696
+ await advanceTimers(300);
697
+ expect(Scheduler).toHaveYielded(['Promise resolved [Outer content]']);
698
+ expect(Scheduler).toFlushAndYield([
699
+ 'Outer content',
700
+ 'Suspend! [Inner content]',
701
+ 'Loading inner...',
702
+ ]);
703
+ // Don't commit the inner placeholder yet.
704
+ expect(ReactNoop.getChildren()).toEqual([
705
+ span('Sync'),
706
+ span('Loading outer...'),
707
+ ]);
708
959
- it('flushes all expired updates in a single batch', async () => {
960
- class Foo extends React.Component {
961
- componentDidUpdate() {
962
- Scheduler.unstable_yieldValue('Commit: ' + this.props.text);
963
- }
964
- componentDidMount() {
965
- Scheduler.unstable_yieldValue('Commit: ' + this.props.text);
966
- }
967
- render() {
968
- return (
709
+ // Expire the inner timeout.
710
+ ReactNoop.expire(500);
711
+ await advanceTimers(500);
712
+ // Now that 750ms have elapsed since the outer placeholder timed out,
713
+ // we can timeout the inner placeholder.
714
+ expect(ReactNoop.getChildren()).toEqual([
715
+ span('Sync'),
716
+ span('Outer content'),
717
+ span('Loading inner...'),
718
+ ]);
719
+
720
+ // Finally, flush the inner promise. We should see the complete screen.
721
+ ReactNoop.expire(1000);
722
+ await advanceTimers(1000);
723
+ expect(Scheduler).toHaveYielded(['Promise resolved [Inner content]']);
724
+ expect(Scheduler).toFlushAndYield(['Inner content']);
725
+ expect(ReactNoop.getChildren()).toEqual([
726
+ span('Sync'),
727
+ span('Outer content'),
728
+ span('Inner content'),
729
+ ]);
730
+ });
731
+
732
+ it('renders an expiration boundary synchronously', async () => {
733
+ spyOnDev(console, 'error');
734
+ // Synchronously render a tree that suspends
735
+ ReactNoop.flushSync(() =>
736
+ ReactNoop.render(
737
+ <Fragment>
738
+ <Suspense fallback={<Text text="Loading..." />}>
739
+ <AsyncText text="Async" />
740
+ </Suspense>
741
+ <Text text="Sync" />
742
+ </Fragment>,
743
+ ),
744
+ );
745
+ expect(Scheduler).toHaveYielded([
746
+ // The async child suspends
747
+ 'Suspend! [Async]',
748
+ // We immediately render the fallback UI
749
+ 'Loading...',
750
+ // Continue on the sibling
751
+ 'Sync',
752
+ ]);
753
+ // The tree commits synchronously
754
+ expect(ReactNoop.getChildren()).toEqual([
755
+ span('Loading...'),
756
+ span('Sync'),
757
+ ]);
758
+
759
+ // Once the promise resolves, we render the suspended view
760
+ await advanceTimers(0);
761
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
762
+ expect(Scheduler).toFlushAndYield(['Async']);
763
+ expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
764
+ });
765
+
766
+ it('suspending inside an expired expiration boundary will bubble to the next one', async () => {
767
+ ReactNoop.flushSync(() =>
768
+ ReactNoop.render(
769
+ <Fragment>
770
+ <Suspense fallback={<Text text="Loading (outer)..." />}>
771
+ <Suspense fallback={<AsyncText text="Loading (inner)..." />}>
772
+ <AsyncText text="Async" />
773
+ </Suspense>
774
+ <Text text="Sync" />
775
+ </Suspense>
776
+ </Fragment>,
777
+ ),
778
+ );
779
+ expect(Scheduler).toHaveYielded([
780
+ 'Suspend! [Async]',
781
+ 'Suspend! [Loading (inner)...]',
782
+ 'Sync',
783
+ 'Loading (outer)...',
784
+ ]);
785
+ // The tree commits synchronously
786
+ expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]);
787
+ });
788
+
789
+ it('expires early by default', async () => {
790
+ ReactNoop.render(
791
+ <Fragment>
792
+ <Suspense fallback={<Text text="Loading..." />} />
793
+ </Fragment>,
794
+ );
795
+ expect(Scheduler).toFlushAndYield([]);
796
+
797
+ ReactNoop.render(
798
+ <Fragment>
799
+ <Suspense fallback={<Text text="Loading..." />}>
800
+ <AsyncText text="Async" ms={3000} />
801
+ </Suspense>
802
+ <Text text="Sync" />
803
+ </Fragment>,
804
+ );
805
+
806
+ expect(Scheduler).toFlushAndYield([
807
+ // The async child suspends
808
+ 'Suspend! [Async]',
809
+ 'Loading...',
810
+ // Continue on the sibling
811
+ 'Sync',
812
+ ]);
813
+ // The update hasn't expired yet, so we commit nothing.
814
+ expect(ReactNoop.getChildren()).toEqual([]);
815
+
816
+ // Advance both React's virtual time and Jest's timers by enough to trigger
817
+ // the timeout, but not by enough to flush the promise or reach the true
818
+ // expiration time.
819
+ ReactNoop.expire(2000);
820
+ await advanceTimers(2000);
821
+ expect(Scheduler).toFlushWithoutYielding();
822
+ expect(ReactNoop.getChildren()).toEqual([
823
+ span('Loading...'),
824
+ span('Sync'),
825
+ ]);
826
+
827
+ // Once the promise resolves, we render the suspended view
828
+ await advanceTimers(1000);
829
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
830
+ expect(Scheduler).toFlushAndYield(['Async']);
831
+ expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
832
+ });
833
+
834
+ it('resolves successfully even if fallback render is pending', async () => {
835
+ ReactNoop.render(
836
+ <>
837
+ <Suspense fallback={<Text text="Loading..." />} />
838
+ </>,
839
+ );
840
+ expect(Scheduler).toFlushAndYield([]);
841
+ expect(ReactNoop.getChildren()).toEqual([]);
842
+ ReactNoop.render(
843
+ <>
844
+ <Suspense fallback={<Text text="Loading..." />}>
845
+ <AsyncText text="Async" ms={3000} />
846
+ </Suspense>
847
+ </>,
848
+ );
849
+ expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
850
+ await advanceTimers(1500);
851
+ expect(Scheduler).toHaveYielded([]);
852
+ expect(ReactNoop.getChildren()).toEqual([]);
853
+ // Before we have a chance to flush, the promise resolves.
854
+ await advanceTimers(2000);
855
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
856
+ expect(Scheduler).toFlushAndYield([
857
+ // We've now pinged the boundary but we don't know if we should restart yet,
858
+ // because we haven't completed the suspense boundary.
859
+ 'Loading...',
860
+ // Once we've completed the boundary we restarted.
861
+ 'Async',
862
+ ]);
863
+ expect(ReactNoop.getChildren()).toEqual([span('Async')]);
864
+ });
865
+
866
+ it('throws a helpful error when an update is suspends without a placeholder', () => {
867
+ ReactNoop.render(<AsyncText ms={1000} text="Async" />);
868
+ expect(Scheduler).toFlushAndThrow(
869
+ 'AsyncText suspended while rendering, but no fallback UI was specified.',
870
+ );
871
+ });
872
+
873
+ it('a Suspense component correctly handles more than one suspended child', async () => {
874
+ ReactNoop.render(
875
<Suspense fallback={<Text text="Loading..." />}>
970
- <AsyncText ms={20000} text={this.props.text} />
971
- </Suspense>
876
+ <AsyncText text="A" ms={100} />
877
+ <AsyncText text="B" ms={100} />
878
+ </Suspense>,
879
);
973
- }
974
- }
975
-
976
- ReactNoop.render(<Foo text="" />);
977
- ReactNoop.expire(1000);
978
- jest.advanceTimersByTime(1000);
979
- ReactNoop.render(<Foo text="go" />);
980
- ReactNoop.expire(1000);
981
- jest.advanceTimersByTime(1000);
982
- ReactNoop.render(<Foo text="good" />);
983
- ReactNoop.expire(1000);
984
- jest.advanceTimersByTime(1000);
985
- ReactNoop.render(<Foo text="goodbye" />);
986
-
987
- Scheduler.unstable_advanceTime(10000);
988
- jest.advanceTimersByTime(10000);
989
-
990
- expect(Scheduler).toFlushExpired([
991
- 'Suspend! [goodbye]',
992
- 'Loading...',
993
- 'Commit: goodbye',
994
- ]);
995
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
996
-
997
- Scheduler.unstable_advanceTime(20000);
998
- await advanceTimers(20000);
999
- expect(Scheduler).toHaveYielded(['Promise resolved [goodbye]']);
1000
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1001
-
1002
- expect(Scheduler).toFlushAndYield(['goodbye']);
1003
- expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1004
- });
880
+ Scheduler.unstable_advanceTime(10000);
881
+ expect(Scheduler).toFlushExpired([
882
+ 'Suspend! [A]',
883
+ 'Suspend! [B]',
884
+ 'Loading...',
885
+ ]);
886
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
887
1006
- it('a suspended update that expires', async () => {
1007
- // Regression test. This test used to fall into an infinite loop.
1008
- function ExpensiveText({text}) {
1009
- // This causes the update to expire.
1010
- Scheduler.unstable_advanceTime(10000);
1011
- // Then something suspends.
1012
- return <AsyncText text={text} ms={200000} />;
1013
- }
1014
-
1015
- function App() {
1016
- return (
1017
- <Suspense fallback="Loading...">
1018
- <ExpensiveText text="A" />
1019
- <ExpensiveText text="B" />
1020
- <ExpensiveText text="C" />
1021
- </Suspense>
1022
- );
1023
- }
1024
-
1025
- ReactNoop.render(<App />);
1026
- expect(Scheduler).toFlushAndYield([
1027
- 'Suspend! [A]',
1028
- 'Suspend! [B]',
1029
- 'Suspend! [C]',
1030
- ]);
1031
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
1032
-
1033
- await advanceTimers(200000);
1034
- expect(Scheduler).toHaveYielded([
1035
- 'Promise resolved [A]',
1036
- 'Promise resolved [B]',
1037
- 'Promise resolved [C]',
1038
- ]);
1039
-
1040
- expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
1041
- expect(ReactNoop).toMatchRenderedOutput(
1042
- <>
1043
- <span prop="A" />
1044
- <span prop="B" />
1045
- <span prop="C" />
1046
- </>,
1047
- );
1048
- });
888
+ await advanceTimers(100);
889
+
890
+ expect(Scheduler).toHaveYielded([
891
+ 'Promise resolved [A]',
892
+ 'Promise resolved [B]',
893
+ ]);
894
+ expect(Scheduler).toFlushAndYield(['A', 'B']);
895
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
896
+ });
897
1050
- describe('legacy mode mode', () => {
1051
- it('times out immediately', async () => {
1052
- function App() {
1053
- return (
898
+ it('can resume rendering earlier than a timeout', async () => {
899
+ ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
900
+ expect(Scheduler).toFlushAndYield([]);
901
+
902
+ ReactNoop.render(
903
<Suspense fallback={<Text text="Loading..." />}>
1055
- <AsyncText ms={100} text="Result" />
1056
- </Suspense>
904
+ <AsyncText text="Async" ms={100} />
905
+ </Suspense>,
906
);
1058
- }
907
+ expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
908
+ expect(ReactNoop.getChildren()).toEqual([]);
909
+
910
+ // Advance time by an amount slightly smaller than what's necessary to
911
+ // resolve the promise
912
+ await advanceTimers(99);
913
+
914
+ // Nothing has rendered yet
915
+ expect(Scheduler).toFlushWithoutYielding();
916
+ expect(ReactNoop.getChildren()).toEqual([]);
917
+
918
+ // Resolve the promise
919
+ await advanceTimers(1);
920
+ // We can now resume rendering
921
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
922
+ expect(Scheduler).toFlushAndYield(['Async']);
923
+ expect(ReactNoop.getChildren()).toEqual([span('Async')]);
924
+ });
925
+
926
+ it('starts working on an update even if its priority falls between two suspended levels', async () => {
927
+ function App(props) {
928
+ return (
929
+ <Suspense fallback={<Text text="Loading..." />}>
930
+ {props.text === 'C' || props.text === 'S' ? (
931
+ <Text text={props.text} />
932
+ ) : (
933
+ <AsyncText text={props.text} ms={10000} />
934
+ )}
935
+ </Suspense>
936
+ );
937
+ }
938
+
939
+ // First mount without suspending. This ensures we already have content
940
+ // showing so that subsequent updates will suspend.
941
+ ReactNoop.render(<App text="S" />);
942
+ expect(Scheduler).toFlushAndYield(['S']);
943
+
944
+ // Schedule an update, and suspend for up to 5 seconds.
945
+ React.unstable_withSuspenseConfig(
946
+ () => ReactNoop.render(<App text="A" />),
947
+ {
948
+ timeoutMs: 5000,
949
+ },
950
+ );
951
+ // The update should suspend.
952
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
953
+ expect(ReactNoop.getChildren()).toEqual([span('S')]);
954
+
955
+ // Advance time until right before it expires.
956
+ await advanceTimers(4999);
957
+ ReactNoop.expire(4999);
958
+ expect(Scheduler).toFlushWithoutYielding();
959
+ expect(ReactNoop.getChildren()).toEqual([span('S')]);
960
+
961
+ // Schedule another low priority update.
962
+ React.unstable_withSuspenseConfig(
963
+ () => ReactNoop.render(<App text="B" />),
964
+ {
965
+ timeoutMs: 10000,
966
+ },
967
+ );
968
+ // This update should also suspend.
969
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
970
+ expect(ReactNoop.getChildren()).toEqual([span('S')]);
971
+
972
+ // Schedule a regular update. Its expiration time will fall between
973
+ // the expiration times of the previous two updates.
974
+ ReactNoop.render(<App text="C" />);
975
+ expect(Scheduler).toFlushAndYield(['C']);
976
+ expect(ReactNoop.getChildren()).toEqual([span('C')]);
977
+
978
+ await advanceTimers(10000);
979
+ // Flush the remaining work.
980
+ expect(Scheduler).toHaveYielded([
981
+ 'Promise resolved [A]',
982
+ 'Promise resolved [B]',
983
+ ]);
984
+ // Nothing else to render.
985
+ expect(Scheduler).toFlushWithoutYielding();
986
+ expect(ReactNoop.getChildren()).toEqual([span('C')]);
987
+ });
988
+
989
+ it('flushes all expired updates in a single batch', async () => {
990
+ class Foo extends React.Component {
991
+ componentDidUpdate() {
992
+ Scheduler.unstable_yieldValue('Commit: ' + this.props.text);
993
+ }
994
+ componentDidMount() {
995
+ Scheduler.unstable_yieldValue('Commit: ' + this.props.text);
996
+ }
997
+ render() {
998
+ return (
999
+ <Suspense fallback={<Text text="Loading..." />}>
1000
+ <AsyncText ms={20000} text={this.props.text} />
1001
+ </Suspense>
1002
+ );
1003
+ }
1004
+ }
1005
+
1006
+ ReactNoop.render(<Foo text="" />);
1007
+ ReactNoop.expire(1000);
1008
+ jest.advanceTimersByTime(1000);
1009
+ ReactNoop.render(<Foo text="go" />);
1010
+ ReactNoop.expire(1000);
1011
+ jest.advanceTimersByTime(1000);
1012
+ ReactNoop.render(<Foo text="good" />);
1013
+ ReactNoop.expire(1000);
1014
+ jest.advanceTimersByTime(1000);
1015
+ ReactNoop.render(<Foo text="goodbye" />);
1016
+
1017
+ Scheduler.unstable_advanceTime(10000);
1018
+ jest.advanceTimersByTime(10000);
1019
+
1020
+ expect(Scheduler).toFlushExpired([
1021
+ 'Suspend! [goodbye]',
1022
+ 'Loading...',
1023
+ 'Commit: goodbye',
1024
+ ]);
1025
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1026
+
1027
+ Scheduler.unstable_advanceTime(20000);
1028
+ await advanceTimers(20000);
1029
+ expect(Scheduler).toHaveYielded(['Promise resolved [goodbye]']);
1030
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1031
+
1032
+ expect(Scheduler).toFlushAndYield(['goodbye']);
1033
+ expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1034
+ });
1035
+
1036
+ it('a suspended update that expires', async () => {
1037
+ // Regression test. This test used to fall into an infinite loop.
1038
+ function ExpensiveText({text}) {
1039
+ // This causes the update to expire.
1040
+ Scheduler.unstable_advanceTime(10000);
1041
+ // Then something suspends.
1042
+ return <AsyncText text={text} ms={200000} />;
1043
+ }
1044
+
1045
+ function App() {
1046
+ return (
1047
+ <Suspense fallback="Loading...">
1048
+ <ExpensiveText text="A" />
1049
+ <ExpensiveText text="B" />
1050
+ <ExpensiveText text="C" />
1051
+ </Suspense>
1052
+ );
1053
+ }
1054
+
1055
+ ReactNoop.render(<App />);
1056
+ expect(Scheduler).toFlushAndYield([
1057
+ 'Suspend! [A]',
1058
+ 'Suspend! [B]',
1059
+ 'Suspend! [C]',
1060
+ ]);
1061
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
1062
+
1063
+ await advanceTimers(200000);
1064
+ expect(Scheduler).toHaveYielded([
1065
+ 'Promise resolved [A]',
1066
+ 'Promise resolved [B]',
1067
+ 'Promise resolved [C]',
1068
+ ]);
1069
+
1070
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
1071
+ expect(ReactNoop).toMatchRenderedOutput(
1072
+ <>
1073
+ <span prop="A" />
1074
+ <span prop="B" />
1075
+ <span prop="C" />
1076
+ </>,
1077
+ );
1078
+ });
1079
+
1080
+ describe('legacy mode mode', () => {
1081
+ it('times out immediately', async () => {
1082
+ function App() {
1083
+ return (
1084
+ <Suspense fallback={<Text text="Loading..." />}>
1085
+ <AsyncText ms={100} text="Result" />
1086
+ </Suspense>
1087
+ );
1088
+ }
1089
+
1090
+ // Times out immediately, ignoring the specified threshold.
1091
+ ReactNoop.renderLegacySyncRoot(<App />);
1092
+ expect(Scheduler).toHaveYielded(['Suspend! [Result]', 'Loading...']);
1093
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1094
+
1095
+ ReactNoop.expire(100);
1096
+ await advanceTimers(100);
1097
+
1098
+ expect(Scheduler).toHaveYielded(['Promise resolved [Result]']);
1099
+ expect(Scheduler).toFlushExpired(['Result']);
1100
+ expect(ReactNoop.getChildren()).toEqual([span('Result')]);
1101
+ });
1102
+
1103
+ it('times out immediately when Suspense is in legacy mode', async () => {
1104
+ class UpdatingText extends React.Component {
1105
+ state = {step: 1};
1106
+ render() {
1107
+ return <AsyncText ms={100} text={`Step: ${this.state.step}`} />;
1108
+ }
1109
+ }
1110
+
1111
+ function Spinner() {
1112
+ return (
1113
+ <Fragment>
1114
+ <Text text="Loading (1)" />
1115
+ <Text text="Loading (2)" />
1116
+ <Text text="Loading (3)" />
1117
+ </Fragment>
1118
+ );
1119
+ }
1120
+
1121
+ const text = React.createRef(null);
1122
+ function App() {
1123
+ return (
1124
+ <Suspense fallback={<Spinner />}>
1125
+ <UpdatingText ref={text} />
1126
+ <Text text="Sibling" />
1127
+ </Suspense>
1128
+ );
1129
+ }
1130
+
1131
+ // Initial mount.
1132
+ ReactNoop.renderLegacySyncRoot(<App />);
1133
+ await advanceTimers(100);
1134
+ expect(Scheduler).toHaveYielded([
1135
+ 'Suspend! [Step: 1]',
1136
+ 'Sibling',
1137
+ 'Loading (1)',
1138
+ 'Loading (2)',
1139
+ 'Loading (3)',
1140
+ 'Promise resolved [Step: 1]',
1141
+ ]);
1142
+ expect(Scheduler).toFlushExpired(['Step: 1']);
1143
+ expect(ReactNoop).toMatchRenderedOutput(
1144
+ <>
1145
+ <span prop="Step: 1" />
1146
+ <span prop="Sibling" />
1147
+ </>,
1148
+ );
1149
+
1150
+ // Update.
1151
+ text.current.setState({step: 2}, () =>
1152
+ Scheduler.unstable_yieldValue('Update did commit'),
1153
+ );
1154
+
1155
+ expect(ReactNoop.flushNextYield()).toEqual([
1156
+ 'Suspend! [Step: 2]',
1157
+ 'Loading (1)',
1158
+ 'Loading (2)',
1159
+ 'Loading (3)',
1160
+ 'Update did commit',
1161
+ ]);
1162
+ expect(ReactNoop).toMatchRenderedOutput(
1163
+ <>
1164
+ <span hidden={true} prop="Step: 1" />
1165
+ <span hidden={true} prop="Sibling" />
1166
+ <span prop="Loading (1)" />
1167
+ <span prop="Loading (2)" />
1168
+ <span prop="Loading (3)" />
1169
+ </>,
1170
+ );
1171
+
1172
+ await advanceTimers(100);
1173
+ expect(Scheduler).toHaveYielded(['Promise resolved [Step: 2]']);
1174
+ expect(Scheduler).toFlushExpired(['Step: 2']);
1175
+ expect(ReactNoop).toMatchRenderedOutput(
1176
+ <>
1177
+ <span prop="Step: 2" />
1178
+ <span prop="Sibling" />
1179
+ </>,
1180
+ );
1181
+ });
1182
+
1183
+ it('does not re-render siblings in loose mode', async () => {
1184
+ class TextWithLifecycle extends React.Component {
1185
+ componentDidMount() {
1186
+ Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1187
+ }
1188
+ componentDidUpdate() {
1189
+ Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1190
+ }
1191
+ render() {
1192
+ return <Text {...this.props} />;
1193
+ }
1194
+ }
1195
+
1196
+ class AsyncTextWithLifecycle extends React.Component {
1197
+ componentDidMount() {
1198
+ Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1199
+ }
1200
+ componentDidUpdate() {
1201
+ Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1202
+ }
1203
+ render() {
1204
+ return <AsyncText {...this.props} />;
1205
+ }
1206
+ }
1207
+
1208
+ function App() {
1209
+ return (
1210
+ <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1211
+ <TextWithLifecycle text="A" />
1212
+ <AsyncTextWithLifecycle ms={100} text="B" />
1213
+ <TextWithLifecycle text="C" />
1214
+ </Suspense>
1215
+ );
1216
+ }
1217
+
1218
+ ReactNoop.renderLegacySyncRoot(<App />, () =>
1219
+ Scheduler.unstable_yieldValue('Commit root'),
1220
+ );
1221
+ expect(Scheduler).toHaveYielded([
1222
+ 'A',
1223
+ 'Suspend! [B]',
1224
+ 'C',
1225
+
1226
+ 'Loading...',
1227
+ 'Mount [A]',
1228
+ 'Mount [B]',
1229
+ 'Mount [C]',
1230
+ // This should be a mount, not an update.
1231
+ 'Mount [Loading...]',
1232
+ 'Commit root',
1233
+ ]);
1234
+ expect(ReactNoop).toMatchRenderedOutput(
1235
+ <>
1236
+ <span hidden={true} prop="A" />
1237
+ <span hidden={true} prop="C" />
1238
+
1239
+ <span prop="Loading..." />
1240
+ </>,
1241
+ );
1242
+
1243
+ ReactNoop.expire(1000);
1244
+ await advanceTimers(1000);
1245
+
1246
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1247
+ expect(Scheduler).toFlushExpired(['B']);
1248
+ expect(ReactNoop).toMatchRenderedOutput(
1249
+ <>
1250
+ <span prop="A" />
1251
+ <span prop="B" />
1252
+ <span prop="C" />
1253
+ </>,
1254
+ );
1255
+ });
1256
+
1257
+ it('suspends inside constructor', async () => {
1258
+ class AsyncTextInConstructor extends React.Component {
1259
+ constructor(props) {
1260
+ super(props);
1261
+ const text = props.text;
1262
+ Scheduler.unstable_yieldValue('constructor');
1263
+ try {
1264
+ TextResource.read([props.text, props.ms]);
1265
+ this.state = {text};
1266
+ } catch (promise) {
1267
+ if (typeof promise.then === 'function') {
1268
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1269
+ } else {
1270
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
1271
+ }
1272
+ throw promise;
1273
+ }
1274
+ }
1275
+ componentDidMount() {
1276
+ Scheduler.unstable_yieldValue('componentDidMount');
1277
+ }
1278
+ render() {
1279
+ Scheduler.unstable_yieldValue(this.state.text);
1280
+ return <span prop={this.state.text} />;
1281
+ }
1282
+ }
1283
+
1284
+ ReactNoop.renderLegacySyncRoot(
1285
+ <Suspense fallback={<Text text="Loading..." />}>
1286
+ <AsyncTextInConstructor ms={100} text="Hi" />
1287
+ </Suspense>,
1288
+ );
1289
+
1290
+ expect(Scheduler).toHaveYielded([
1291
+ 'constructor',
1292
+ 'Suspend! [Hi]',
1293
+ 'Loading...',
1294
+ ]);
1295
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1296
+
1297
+ await advanceTimers(1000);
1298
+
1299
+ expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1300
+ expect(Scheduler).toFlushExpired([
1301
+ 'constructor',
1302
+ 'Hi',
1303
+ 'componentDidMount',
1304
+ ]);
1305
+ expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1306
+ });
1307
+
1308
+ it('does not infinite loop if fallback contains lifecycle method', async () => {
1309
+ class Fallback extends React.Component {
1310
+ state = {
1311
+ name: 'foo',
1312
+ };
1313
+ componentDidMount() {
1314
+ this.setState({
1315
+ name: 'bar',
1316
+ });
1317
+ }
1318
+ render() {
1319
+ return <Text text="Loading..." />;
1320
+ }
1321
+ }
1322
+
1323
+ class Demo extends React.Component {
1324
+ render() {
1325
+ return (
1326
+ <Suspense fallback={<Fallback />}>
1327
+ <AsyncText text="Hi" ms={100} />
1328
+ </Suspense>
1329
+ );
1330
+ }
1331
+ }
1332
1060
- // Times out immediately, ignoring the specified threshold.
1061
- ReactNoop.renderLegacySyncRoot(<App />);
1062
- expect(Scheduler).toHaveYielded(['Suspend! [Result]', 'Loading...']);
1063
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1333
+ ReactNoop.renderLegacySyncRoot(<Demo />);
1334
+
1335
+ expect(Scheduler).toHaveYielded([
1336
+ 'Suspend! [Hi]',
1337
+ 'Loading...',
1338
+ // Re-render due to lifecycle update
1339
+ 'Loading...',
1340
+ ]);
1341
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1342
+ await advanceTimers(100);
1343
+ expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1344
+ expect(Scheduler).toFlushExpired(['Hi']);
1345
+ expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1346
+ });
1347
+
1348
+ if (global.__PERSISTENT__) {
1349
+ it('hides/unhides suspended children before layout effects fire (persistent)', async () => {
1350
+ const {useRef, useLayoutEffect} = React;
1351
+
1352
+ function Parent() {
1353
+ const child = useRef(null);
1354
+
1355
+ useLayoutEffect(() => {
1356
+ Scheduler.unstable_yieldValue(
1357
+ ReactNoop.getPendingChildrenAsJSX(),
1358
+ );
1359
+ });
1360
+
1361
+ return (
1362
+ <span ref={child} hidden={false}>
1363
+ <AsyncText ms={1000} text="Hi" />
1364
+ </span>
1365
+ );
1366
+ }
1367
1065
- ReactNoop.expire(100);
1066
- await advanceTimers(100);
1368
+ function App(props) {
1369
+ return (
1370
+ <Suspense fallback={<Text text="Loading..." />}>
1371
+ <Parent />
1372
+ </Suspense>
1373
+ );
1374
+ }
1375
1068
- expect(Scheduler).toHaveYielded(['Promise resolved [Result]']);
1069
- expect(Scheduler).toFlushExpired(['Result']);
1070
- expect(ReactNoop.getChildren()).toEqual([span('Result')]);
1071
- });
1376
+ ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
1377
1073
- it('times out immediately when Suspense is in legacy mode', async () => {
1074
- class UpdatingText extends React.Component {
1075
- state = {step: 1};
1076
- render() {
1077
- return <AsyncText ms={100} text={`Step: ${this.state.step}`} />;
1078
- }
1079
- }
1378
+ expect(Scheduler).toHaveYielded([
1379
+ 'Suspend! [Hi]',
1380
+ 'Loading...',
1381
+ // The child should have already been hidden
1382
+ <>
1383
+ <span hidden={true} />
1384
+ <span prop="Loading..." />
1385
+ </>,
1386
+ ]);
1387
1081
- function Spinner() {
1082
- return (
1083
- <Fragment>
1084
- <Text text="Loading (1)" />
1085
- <Text text="Loading (2)" />
1086
- <Text text="Loading (3)" />
1087
- </Fragment>
1088
- );
1089
- }
1388
+ await advanceTimers(1000);
1389
1091
- const text = React.createRef(null);
1092
- function App() {
1093
- return (
1094
- <Suspense fallback={<Spinner />}>
1095
- <UpdatingText ref={text} />
1096
- <Text text="Sibling" />
1097
- </Suspense>
1098
- );
1099
- }
1390
+ expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1391
+ expect(Scheduler).toFlushExpired(['Hi']);
1392
+ });
1393
+ } else {
1394
+ it('hides/unhides suspended children before layout effects fire (mutation)', async () => {
1395
+ const {useRef, useLayoutEffect} = React;
1396
+
1397
+ function Parent() {
1398
+ const child = useRef(null);
1399
+
1400
+ useLayoutEffect(() => {
1401
+ Scheduler.unstable_yieldValue(
1402
+ 'Child is hidden: ' + child.current.hidden,
1403
+ );
1404
+ });
1405
+
1406
+ return (
1407
+ <span ref={child} hidden={false}>
1408
+ <AsyncText ms={1000} text="Hi" />
1409
+ </span>
1410
+ );
1411
+ }
1412
1101
- // Initial mount.
1102
- ReactNoop.renderLegacySyncRoot(<App />);
1103
- await advanceTimers(100);
1104
- expect(Scheduler).toHaveYielded([
1105
- 'Suspend! [Step: 1]',
1106
- 'Sibling',
1107
- 'Loading (1)',
1108
- 'Loading (2)',
1109
- 'Loading (3)',
1110
- 'Promise resolved [Step: 1]',
1111
- ]);
1112
- expect(Scheduler).toFlushExpired(['Step: 1']);
1113
- expect(ReactNoop).toMatchRenderedOutput(
1114
- <>
1115
- <span prop="Step: 1" />
1116
- <span prop="Sibling" />
1117
- </>,
1118
- );
1413
+ function App(props) {
1414
+ return (
1415
+ <Suspense fallback={<Text text="Loading..." />}>
1416
+ <Parent />
1417
+ </Suspense>
1418
+ );
1419
+ }
1420
1120
- // Update.
1121
- text.current.setState({step: 2}, () =>
1122
- Scheduler.unstable_yieldValue('Update did commit'),
1123
- );
1421
+ ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
1422
1125
- expect(ReactNoop.flushNextYield()).toEqual([
1126
- 'Suspend! [Step: 2]',
1127
- 'Loading (1)',
1128
- 'Loading (2)',
1129
- 'Loading (3)',
1130
- 'Update did commit',
1131
- ]);
1132
- expect(ReactNoop).toMatchRenderedOutput(
1133
- <>
1134
- <span hidden={true} prop="Step: 1" />
1135
- <span hidden={true} prop="Sibling" />
1136
- <span prop="Loading (1)" />
1137
- <span prop="Loading (2)" />
1138
- <span prop="Loading (3)" />
1139
- </>,
1140
- );
1423
+ expect(Scheduler).toHaveYielded([
1424
+ 'Suspend! [Hi]',
1425
+ 'Loading...',
1426
+ // The child should have already been hidden
1427
+ 'Child is hidden: true',
1428
+ ]);
1429
1142
- await advanceTimers(100);
1143
- expect(Scheduler).toHaveYielded(['Promise resolved [Step: 2]']);
1144
- expect(Scheduler).toFlushExpired(['Step: 2']);
1145
- expect(ReactNoop).toMatchRenderedOutput(
1146
- <>
1147
- <span prop="Step: 2" />
1148
- <span prop="Sibling" />
1149
- </>,
1150
- );
1151
- });
1430
+ await advanceTimers(1000);
1431
1153
- it('does not re-render siblings in loose mode', async () => {
1154
- class TextWithLifecycle extends React.Component {
1155
- componentDidMount() {
1156
- Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1157
- }
1158
- componentDidUpdate() {
1159
- Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1160
- }
1161
- render() {
1162
- return <Text {...this.props} />;
1432
+ expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1433
+ expect(Scheduler).toFlushExpired(['Hi']);
1434
+ });
1435
}
1164
- }
1436
1166
- class AsyncTextWithLifecycle extends React.Component {
1167
- componentDidMount() {
1168
- Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1437
+ it('handles errors in the return path of a component that suspends', async () => {
1438
+ // Covers an edge case where an error is thrown inside the complete phase
1439
+ // of a component that is in the return path of a component that suspends.
1440
+ // The second error should also be handled (i.e. able to be captured by
1441
+ // an error boundary.
1442
+ class ErrorBoundary extends React.Component {
1443
+ state = {error: null};
1444
+ static getDerivedStateFromError(error, errorInfo) {
1445
+ return {error};
1446
+ }
1447
+ render() {
1448
+ if (this.state.error) {
1449
+ return `Caught an error: ${this.state.error.message}`;
1450
+ }
1451
+ return this.props.children;
1452
+ }
1453
+ }
1454
+
1455
+ ReactNoop.renderLegacySyncRoot(
1456
+ <ErrorBoundary>
1457
+ <Suspense fallback="Loading...">
1458
+ <errorInCompletePhase>
1459
+ <AsyncText ms={1000} text="Async" />
1460
+ </errorInCompletePhase>
1461
+ </Suspense>
1462
+ </ErrorBoundary>,
1463
+ );
1464
+
1465
+ expect(Scheduler).toHaveYielded(['Suspend! [Async]']);
1466
+ expect(ReactNoop).toMatchRenderedOutput(
1467
+ 'Caught an error: Error in host config.',
1468
+ );
1469
+ });
1470
+ });
1471
+
1472
+ it('does not call lifecycles of a suspended component', async () => {
1473
+ class TextWithLifecycle extends React.Component {
1474
+ componentDidMount() {
1475
+ Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1476
+ }
1477
+ componentDidUpdate() {
1478
+ Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1479
+ }
1480
+ componentWillUnmount() {
1481
+ Scheduler.unstable_yieldValue(`Unmount [${this.props.text}]`);
1482
+ }
1483
+ render() {
1484
+ return <Text {...this.props} />;
1485
+ }
1486
}
1170
- componentDidUpdate() {
1171
- Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1487
+
1488
+ class AsyncTextWithLifecycle extends React.Component {
1489
+ componentDidMount() {
1490
+ Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1491
+ }
1492
+ componentDidUpdate() {
1493
+ Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1494
+ }
1495
+ componentWillUnmount() {
1496
+ Scheduler.unstable_yieldValue(`Unmount [${this.props.text}]`);
1497
+ }
1498
+ render() {
1499
+ const text = this.props.text;
1500
+ const ms = this.props.ms;
1501
+ try {
1502
+ TextResource.read([text, ms]);
1503
+ Scheduler.unstable_yieldValue(text);
1504
+ return <span prop={text} />;
1505
+ } catch (promise) {
1506
+ if (typeof promise.then === 'function') {
1507
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1508
+ } else {
1509
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
1510
+ }
1511
+ throw promise;
1512
+ }
1513
+ }
1514
}
1173
- render() {
1174
- return <AsyncText {...this.props} />;
1515
+
1516
+ function App() {
1517
+ return (
1518
+ <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1519
+ <TextWithLifecycle text="A" />
1520
+ <AsyncTextWithLifecycle ms={100} text="B" />
1521
+ <TextWithLifecycle text="C" />
1522
+ </Suspense>
1523
+ );
1524
}
1176
- }
1525
1178
- function App() {
1179
- return (
1180
- <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1181
- <TextWithLifecycle text="A" />
1182
- <AsyncTextWithLifecycle ms={100} text="B" />
1183
- <TextWithLifecycle text="C" />
1184
- </Suspense>
1526
+ ReactNoop.renderLegacySyncRoot(<App />, () =>
1527
+ Scheduler.unstable_yieldValue('Commit root'),
1528
);
1186
- }
1529
+ expect(Scheduler).toHaveYielded([
1530
+ 'A',
1531
+ 'Suspend! [B]',
1532
+ 'C',
1533
+ 'Loading...',
1534
1188
- ReactNoop.renderLegacySyncRoot(<App />, () =>
1189
- Scheduler.unstable_yieldValue('Commit root'),
1190
- );
1191
- expect(Scheduler).toHaveYielded([
1192
- 'A',
1193
- 'Suspend! [B]',
1194
- 'C',
1195
-
1196
- 'Loading...',
1197
- 'Mount [A]',
1198
- 'Mount [B]',
1199
- 'Mount [C]',
1200
- // This should be a mount, not an update.
1201
- 'Mount [Loading...]',
1202
- 'Commit root',
1203
- ]);
1204
- expect(ReactNoop).toMatchRenderedOutput(
1205
- <>
1206
- <span hidden={true} prop="A" />
1207
- <span hidden={true} prop="C" />
1208
-
1209
- <span prop="Loading..." />
1210
- </>,
1211
- );
1535
+ 'Mount [A]',
1536
+ // B's lifecycle should not fire because it suspended
1537
+ // 'Mount [B]',
1538
+ 'Mount [C]',
1539
+ 'Mount [Loading...]',
1540
+ 'Commit root',
1541
+ ]);
1542
+ expect(ReactNoop).toMatchRenderedOutput(
1543
+ <>
1544
+ <span hidden={true} prop="A" />
1545
+ <span hidden={true} prop="C" />
1546
+ <span prop="Loading..." />
1547
+ </>,
1548
+ );
1549
+ });
1550
1213
- ReactNoop.expire(1000);
1214
- await advanceTimers(1000);
1215
-
1216
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1217
- expect(Scheduler).toFlushExpired(['B']);
1218
- expect(ReactNoop).toMatchRenderedOutput(
1219
- <>
1220
- <span prop="A" />
1221
- <span prop="B" />
1222
- <span prop="C" />
1223
- </>,
1224
- );
1225
- });
1551
+ it('does not call lifecycles of a suspended component (hooks)', async () => {
1552
+ function TextWithLifecycle(props) {
1553
+ React.useLayoutEffect(() => {
1554
+ Scheduler.unstable_yieldValue(`Layout Effect [${props.text}]`);
1555
+ return () => {
1556
+ Scheduler.unstable_yieldValue(
1557
+ `Destroy Layout Effect [${props.text}]`,
1558
+ );
1559
+ };
1560
+ }, [props.text]);
1561
+ React.useEffect(() => {
1562
+ Scheduler.unstable_yieldValue(`Effect [${props.text}]`);
1563
+ return () => {
1564
+ Scheduler.unstable_yieldValue(`Destroy Effect [${props.text}]`);
1565
+ };
1566
+ }, [props.text]);
1567
+ return <Text {...props} />;
1568
+ }
1569
1227
- it('suspends inside constructor', async () => {
1228
- class AsyncTextInConstructor extends React.Component {
1229
- constructor(props) {
1230
- super(props);
1570
+ function AsyncTextWithLifecycle(props) {
1571
+ React.useLayoutEffect(() => {
1572
+ Scheduler.unstable_yieldValue(`Layout Effect [${props.text}]`);
1573
+ return () => {
1574
+ Scheduler.unstable_yieldValue(
1575
+ `Destroy Layout Effect [${props.text}]`,
1576
+ );
1577
+ };
1578
+ }, [props.text]);
1579
+ React.useEffect(() => {
1580
+ Scheduler.unstable_yieldValue(`Effect [${props.text}]`);
1581
+ return () => {
1582
+ Scheduler.unstable_yieldValue(`Destroy Effect [${props.text}]`);
1583
+ };
1584
+ }, [props.text]);
1585
const text = props.text;
1232
- Scheduler.unstable_yieldValue('constructor');
1586
+ const ms = props.ms;
1587
try {
1234
- TextResource.read([props.text, props.ms]);
1235
- this.state = {text};
1588
+ TextResource.read([text, ms]);
1589
+ Scheduler.unstable_yieldValue(text);
1590
+ return <span prop={text} />;
1591
} catch (promise) {
1592
if (typeof promise.then === 'function') {
1593
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
@@ -1242,1648 +1597,1378 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1597
throw promise;
1598
}
1599
}
1245
- componentDidMount() {
1246
- Scheduler.unstable_yieldValue('componentDidMount');
1247
- }
1248
- render() {
1249
- Scheduler.unstable_yieldValue(this.state.text);
1250
- return <span prop={this.state.text} />;
1251
- }
1252
- }
1253
-
1254
- ReactNoop.renderLegacySyncRoot(
1255
- <Suspense fallback={<Text text="Loading..." />}>
1256
- <AsyncTextInConstructor ms={100} text="Hi" />
1257
- </Suspense>,
1258
- );
1259
-
1260
- expect(Scheduler).toHaveYielded([
1261
- 'constructor',
1262
- 'Suspend! [Hi]',
1263
- 'Loading...',
1264
- ]);
1265
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1266
-
1267
- await advanceTimers(1000);
1268
-
1269
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1270
- expect(Scheduler).toFlushExpired([
1271
- 'constructor',
1272
- 'Hi',
1273
- 'componentDidMount',
1274
- ]);
1275
- expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1276
- });
1277
-
1278
- it('does not infinite loop if fallback contains lifecycle method', async () => {
1279
- class Fallback extends React.Component {
1280
- state = {
1281
- name: 'foo',
1282
- };
1283
- componentDidMount() {
1284
- this.setState({
1285
- name: 'bar',
1286
- });
1287
- }
1288
- render() {
1289
- return <Text text="Loading..." />;
1290
- }
1291
- }
1600
1293
- class Demo extends React.Component {
1294
- render() {
1601
+ function App({text}) {
1602
return (
1296
- <Suspense fallback={<Fallback />}>
1297
- <AsyncText text="Hi" ms={100} />
1603
+ <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1604
+ <TextWithLifecycle text="A" />
1605
+ <AsyncTextWithLifecycle ms={100} text={text} />
1606
+ <TextWithLifecycle text="C" />
1607
</Suspense>
1608
);
1609
}
1301
- }
1610
1303
- ReactNoop.renderLegacySyncRoot(<Demo />);
1304
-
1305
- expect(Scheduler).toHaveYielded([
1306
- 'Suspend! [Hi]',
1307
- 'Loading...',
1308
- // Re-render due to lifecycle update
1309
- 'Loading...',
1310
- ]);
1311
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1312
- await advanceTimers(100);
1313
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1314
- expect(Scheduler).toFlushExpired(['Hi']);
1315
- expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1316
- });
1317
-
1318
- if (global.__PERSISTENT__) {
1319
- it('hides/unhides suspended children before layout effects fire (persistent)', async () => {
1320
- const {useRef, useLayoutEffect} = React;
1611
+ ReactNoop.renderLegacySyncRoot(<App text="B" />, () =>
1612
+ Scheduler.unstable_yieldValue('Commit root'),
1613
+ );
1614
+ expect(Scheduler).toHaveYielded([
1615
+ 'A',
1616
+ 'Suspend! [B]',
1617
+ 'C',
1618
+ 'Loading...',
1619
1322
- function Parent() {
1323
- const child = useRef(null);
1620
+ 'Layout Effect [A]',
1621
+ // B's effect should not fire because it suspended
1622
+ // 'Layout Effect [B]',
1623
+ 'Layout Effect [C]',
1624
+ 'Layout Effect [Loading...]',
1625
+ 'Commit root',
1626
+ ]);
1627
1325
- useLayoutEffect(() => {
1326
- Scheduler.unstable_yieldValue(ReactNoop.getPendingChildrenAsJSX());
1327
- });
1628
+ // Flush passive effects.
1629
+ expect(Scheduler).toFlushAndYield([
1630
+ 'Effect [A]',
1631
+ // B's effect should not fire because it suspended
1632
+ // 'Effect [B]',
1633
+ 'Effect [C]',
1634
+ 'Effect [Loading...]',
1635
+ ]);
1636
1329
- return (
1330
- <span ref={child} hidden={false}>
1331
- <AsyncText ms={1000} text="Hi" />
1332
- </span>
1333
- );
1334
- }
1637
+ expect(ReactNoop).toMatchRenderedOutput(
1638
+ <>
1639
+ <span hidden={true} prop="A" />
1640
+ <span hidden={true} prop="C" />
1641
+ <span prop="Loading..." />
1642
+ </>,
1643
+ );
1644
1336
- function App(props) {
1337
- return (
1338
- <Suspense fallback={<Text text="Loading..." />}>
1339
- <Parent />
1340
- </Suspense>
1341
- );
1645
+ Scheduler.unstable_advanceTime(500);
1646
+ await advanceTimers(500);
1647
+
1648
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1649
+
1650
+ if (
1651
+ ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount &&
1652
+ ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates
1653
+ ) {
1654
+ expect(Scheduler).toFlushAndYield([
1655
+ 'B',
1656
+ 'Destroy Layout Effect [Loading...]',
1657
+ 'Layout Effect [B]',
1658
+ 'Destroy Effect [Loading...]',
1659
+ 'Effect [B]',
1660
+ ]);
1661
+ } else {
1662
+ expect(Scheduler).toFlushAndYield([
1663
+ 'B',
1664
+ 'Destroy Layout Effect [Loading...]',
1665
+ 'Destroy Effect [Loading...]',
1666
+ 'Layout Effect [B]',
1667
+ 'Effect [B]',
1668
+ ]);
1669
}
1670
1344
- ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
1671
+ // Update
1672
+ ReactNoop.renderLegacySyncRoot(<App text="B2" />, () =>
1673
+ Scheduler.unstable_yieldValue('Commit root'),
1674
+ );
1675
1676
expect(Scheduler).toHaveYielded([
1347
- 'Suspend! [Hi]',
1677
+ 'A',
1678
+ 'Suspend! [B2]',
1679
+ 'C',
1680
'Loading...',
1349
- // The child should have already been hidden
1350
- <>
1351
- <span hidden={true} />
1352
- <span prop="Loading..." />
1353
- </>,
1681
+
1682
+ // B2's effect should not fire because it suspended
1683
+ // 'Layout Effect [B2]',
1684
+ 'Layout Effect [Loading...]',
1685
+ 'Commit root',
1686
]);
1687
1356
- await advanceTimers(1000);
1688
+ // Flush passive effects.
1689
+ expect(Scheduler).toFlushAndYield([
1690
+ // B2's effect should not fire because it suspended
1691
+ // 'Effect [B2]',
1692
+ 'Effect [Loading...]',
1693
+ ]);
1694
1358
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1359
- expect(Scheduler).toFlushExpired(['Hi']);
1695
+ Scheduler.unstable_advanceTime(500);
1696
+ await advanceTimers(500);
1697
+
1698
+ expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
1699
+
1700
+ if (
1701
+ ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount &&
1702
+ ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates
1703
+ ) {
1704
+ expect(Scheduler).toFlushAndYield([
1705
+ 'B2',
1706
+ 'Destroy Layout Effect [Loading...]',
1707
+ 'Destroy Layout Effect [B]',
1708
+ 'Layout Effect [B2]',
1709
+ 'Destroy Effect [Loading...]',
1710
+ 'Destroy Effect [B]',
1711
+ 'Effect [B2]',
1712
+ ]);
1713
+ } else {
1714
+ expect(Scheduler).toFlushAndYield([
1715
+ 'B2',
1716
+ 'Destroy Layout Effect [Loading...]',
1717
+ 'Destroy Effect [Loading...]',
1718
+ 'Destroy Layout Effect [B]',
1719
+ 'Layout Effect [B2]',
1720
+ 'Destroy Effect [B]',
1721
+ 'Effect [B2]',
1722
+ ]);
1723
+ }
1724
});
1361
- } else {
1362
- it('hides/unhides suspended children before layout effects fire (mutation)', async () => {
1363
- const {useRef, useLayoutEffect} = React;
1364
-
1365
- function Parent() {
1366
- const child = useRef(null);
1367
-
1368
- useLayoutEffect(() => {
1369
- Scheduler.unstable_yieldValue(
1370
- 'Child is hidden: ' + child.current.hidden,
1371
- );
1372
- });
1725
1726
+ it('suspends for longer if something took a long (CPU bound) time to render', async () => {
1727
+ function Foo({renderContent}) {
1728
+ Scheduler.unstable_yieldValue('Foo');
1729
return (
1375
- <span ref={child} hidden={false}>
1376
- <AsyncText ms={1000} text="Hi" />
1377
- </span>
1730
+ <Suspense fallback={<Text text="Loading..." />}>
1731
+ {renderContent ? <AsyncText text="A" ms={5000} /> : null}
1732
+ </Suspense>
1733
);
1734
}
1735
1381
- function App(props) {
1736
+ ReactNoop.render(<Foo />);
1737
+ expect(Scheduler).toFlushAndYield(['Foo']);
1738
+
1739
+ ReactNoop.render(<Foo renderContent={true} />);
1740
+ Scheduler.unstable_advanceTime(100);
1741
+ await advanceTimers(100);
1742
+ // Start rendering
1743
+ expect(Scheduler).toFlushAndYieldThrough(['Foo']);
1744
+ // For some reason it took a long time to render Foo.
1745
+ Scheduler.unstable_advanceTime(1250);
1746
+ await advanceTimers(1250);
1747
+ expect(Scheduler).toFlushAndYield([
1748
+ // A suspends
1749
+ 'Suspend! [A]',
1750
+ 'Loading...',
1751
+ ]);
1752
+ // We're now suspended and we haven't shown anything yet.
1753
+ expect(ReactNoop.getChildren()).toEqual([]);
1754
+
1755
+ // Flush some of the time
1756
+ Scheduler.unstable_advanceTime(450);
1757
+ await advanceTimers(450);
1758
+ // Because we've already been waiting for so long we can
1759
+ // wait a bit longer. Still nothing...
1760
+ expect(Scheduler).toFlushWithoutYielding();
1761
+ expect(ReactNoop.getChildren()).toEqual([]);
1762
+
1763
+ // Eventually we'll show the fallback.
1764
+ Scheduler.unstable_advanceTime(500);
1765
+ await advanceTimers(500);
1766
+ // No need to rerender.
1767
+ expect(Scheduler).toFlushWithoutYielding();
1768
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1769
+
1770
+ // Flush the promise completely
1771
+ Scheduler.unstable_advanceTime(4500);
1772
+ await advanceTimers(4500);
1773
+ // Renders successfully
1774
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1775
+ expect(Scheduler).toFlushAndYield(['A']);
1776
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
1777
+ });
1778
+
1779
+ it('does not suspends if a fallback has been shown for a long time', async () => {
1780
+ function Foo() {
1781
+ Scheduler.unstable_yieldValue('Foo');
1782
return (
1783
<Suspense fallback={<Text text="Loading..." />}>
1384
- <Parent />
1784
+ <AsyncText text="A" ms={5000} />
1785
+ <Suspense fallback={<Text text="Loading more..." />}>
1786
+ <AsyncText text="B" ms={10000} />
1787
+ </Suspense>
1788
</Suspense>
1789
);
1790
}
1791
1389
- ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
1390
-
1391
- expect(Scheduler).toHaveYielded([
1392
- 'Suspend! [Hi]',
1792
+ ReactNoop.render(<Foo />);
1793
+ // Start rendering
1794
+ expect(Scheduler).toFlushAndYield([
1795
+ 'Foo',
1796
+ // A suspends
1797
+ 'Suspend! [A]',
1798
+ // B suspends
1799
+ 'Suspend! [B]',
1800
+ 'Loading more...',
1801
'Loading...',
1394
- // The child should have already been hidden
1395
- 'Child is hidden: true',
1802
]);
1803
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1804
1398
- await advanceTimers(1000);
1805
+ // Wait a long time.
1806
+ Scheduler.unstable_advanceTime(5000);
1807
+ await advanceTimers(5000);
1808
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1809
+
1810
+ // Retry with the new content.
1811
+ expect(Scheduler).toFlushAndYield([
1812
+ 'A',
1813
+ // B still suspends
1814
+ 'Suspend! [B]',
1815
+ 'Loading more...',
1816
+ ]);
1817
+ // Because we've already been waiting for so long we've exceeded
1818
+ // our threshold and we show the next level immediately.
1819
+ expect(ReactNoop.getChildren()).toEqual([
1820
+ span('A'),
1821
+ span('Loading more...'),
1822
+ ]);
1823
1400
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1401
- expect(Scheduler).toFlushExpired(['Hi']);
1824
+ // Flush the last promise completely
1825
+ Scheduler.unstable_advanceTime(5000);
1826
+ await advanceTimers(5000);
1827
+ // Renders successfully
1828
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1829
+ expect(Scheduler).toFlushAndYield(['B']);
1830
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1831
});
1403
- }
1404
-
1405
- it('handles errors in the return path of a component that suspends', async () => {
1406
- // Covers an edge case where an error is thrown inside the complete phase
1407
- // of a component that is in the return path of a component that suspends.
1408
- // The second error should also be handled (i.e. able to be captured by
1409
- // an error boundary.
1410
- class ErrorBoundary extends React.Component {
1411
- state = {error: null};
1412
- static getDerivedStateFromError(error, errorInfo) {
1413
- return {error};
1414
- }
1415
- render() {
1416
- if (this.state.error) {
1417
- return `Caught an error: ${this.state.error.message}`;
1418
- }
1419
- return this.props.children;
1832
+
1833
+ it('does suspend if a fallback has been shown for a short time', async () => {
1834
+ function Foo() {
1835
+ Scheduler.unstable_yieldValue('Foo');
1836
+ return (
1837
+ <Suspense fallback={<Text text="Loading..." />}>
1838
+ <AsyncText text="A" ms={200} />
1839
+ <Suspense fallback={<Text text="Loading more..." />}>
1840
+ <AsyncText text="B" ms={450} />
1841
+ </Suspense>
1842
+ </Suspense>
1843
+ );
1844
}
1421
- }
1845
1423
- ReactNoop.renderLegacySyncRoot(
1424
- <ErrorBoundary>
1425
- <Suspense fallback="Loading...">
1426
- <errorInCompletePhase>
1427
- <AsyncText ms={1000} text="Async" />
1428
- </errorInCompletePhase>
1429
- </Suspense>
1430
- </ErrorBoundary>,
1431
- );
1846
+ ReactNoop.render(<Foo />);
1847
+ // Start rendering
1848
+ expect(Scheduler).toFlushAndYield([
1849
+ 'Foo',
1850
+ // A suspends
1851
+ 'Suspend! [A]',
1852
+ // B suspends
1853
+ 'Suspend! [B]',
1854
+ 'Loading more...',
1855
+ 'Loading...',
1856
+ ]);
1857
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1858
1433
- expect(Scheduler).toHaveYielded(['Suspend! [Async]']);
1434
- expect(ReactNoop).toMatchRenderedOutput(
1435
- 'Caught an error: Error in host config.',
1436
- );
1437
- });
1438
- });
1859
+ // Wait a short time.
1860
+ Scheduler.unstable_advanceTime(250);
1861
+ await advanceTimers(250);
1862
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1863
+
1864
+ // Retry with the new content.
1865
+ expect(Scheduler).toFlushAndYield([
1866
+ 'A',
1867
+ // B still suspends
1868
+ 'Suspend! [B]',
1869
+ 'Loading more...',
1870
+ ]);
1871
+ // Because we've already been waiting for so long we can
1872
+ // wait a bit longer. Still nothing...
1873
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1874
1440
- it('does not call lifecycles of a suspended component', async () => {
1441
- class TextWithLifecycle extends React.Component {
1442
- componentDidMount() {
1443
- Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1444
- }
1445
- componentDidUpdate() {
1446
- Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1447
- }
1448
- componentWillUnmount() {
1449
- Scheduler.unstable_yieldValue(`Unmount [${this.props.text}]`);
1450
- }
1451
- render() {
1452
- return <Text {...this.props} />;
1453
- }
1454
- }
1875
+ Scheduler.unstable_advanceTime(200);
1876
+ await advanceTimers(200);
1877
1456
- class AsyncTextWithLifecycle extends React.Component {
1457
- componentDidMount() {
1458
- Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1459
- }
1460
- componentDidUpdate() {
1461
- Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1462
- }
1463
- componentWillUnmount() {
1464
- Scheduler.unstable_yieldValue(`Unmount [${this.props.text}]`);
1465
- }
1466
- render() {
1467
- const text = this.props.text;
1468
- const ms = this.props.ms;
1469
- try {
1470
- TextResource.read([text, ms]);
1471
- Scheduler.unstable_yieldValue(text);
1472
- return <span prop={text} />;
1473
- } catch (promise) {
1474
- if (typeof promise.then === 'function') {
1475
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1476
- } else {
1477
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
1478
- }
1479
- throw promise;
1480
- }
1481
- }
1482
- }
1483
-
1484
- function App() {
1485
- return (
1486
- <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1487
- <TextWithLifecycle text="A" />
1488
- <AsyncTextWithLifecycle ms={100} text="B" />
1489
- <TextWithLifecycle text="C" />
1490
- </Suspense>
1491
- );
1492
- }
1493
-
1494
- ReactNoop.renderLegacySyncRoot(<App />, () =>
1495
- Scheduler.unstable_yieldValue('Commit root'),
1496
- );
1497
- expect(Scheduler).toHaveYielded([
1498
- 'A',
1499
- 'Suspend! [B]',
1500
- 'C',
1501
- 'Loading...',
1502
-
1503
- 'Mount [A]',
1504
- // B's lifecycle should not fire because it suspended
1505
- // 'Mount [B]',
1506
- 'Mount [C]',
1507
- 'Mount [Loading...]',
1508
- 'Commit root',
1509
- ]);
1510
- expect(ReactNoop).toMatchRenderedOutput(
1511
- <>
1512
- <span hidden={true} prop="A" />
1513
- <span hidden={true} prop="C" />
1514
- <span prop="Loading..." />
1515
- </>,
1516
- );
1517
- });
1878
+ // Before we commit another Promise resolves.
1879
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1880
+ // We're still showing the first loading state.
1881
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1882
+ // Restart and render the complete content.
1883
+ expect(Scheduler).toFlushAndYield(['A', 'B']);
1884
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1885
+ });
1886
1519
- it('does not call lifecycles of a suspended component (hooks)', async () => {
1520
- function TextWithLifecycle(props) {
1521
- React.useLayoutEffect(() => {
1522
- Scheduler.unstable_yieldValue(`Layout Effect [${props.text}]`);
1523
- return () => {
1524
- Scheduler.unstable_yieldValue(
1525
- `Destroy Layout Effect [${props.text}]`,
1526
- );
1527
- };
1528
- }, [props.text]);
1529
- React.useEffect(() => {
1530
- Scheduler.unstable_yieldValue(`Effect [${props.text}]`);
1531
- return () => {
1532
- Scheduler.unstable_yieldValue(`Destroy Effect [${props.text}]`);
1533
- };
1534
- }, [props.text]);
1535
- return <Text {...props} />;
1536
- }
1537
-
1538
- function AsyncTextWithLifecycle(props) {
1539
- React.useLayoutEffect(() => {
1540
- Scheduler.unstable_yieldValue(`Layout Effect [${props.text}]`);
1541
- return () => {
1542
- Scheduler.unstable_yieldValue(
1543
- `Destroy Layout Effect [${props.text}]`,
1887
+ it('does not suspend for very long after a higher priority update', async () => {
1888
+ function Foo({renderContent}) {
1889
+ Scheduler.unstable_yieldValue('Foo');
1890
+ return (
1891
+ <Suspense fallback={<Text text="Loading..." />}>
1892
+ {renderContent ? <AsyncText text="A" ms={5000} /> : null}
1893
+ </Suspense>
1894
);
1545
- };
1546
- }, [props.text]);
1547
- React.useEffect(() => {
1548
- Scheduler.unstable_yieldValue(`Effect [${props.text}]`);
1549
- return () => {
1550
- Scheduler.unstable_yieldValue(`Destroy Effect [${props.text}]`);
1551
- };
1552
- }, [props.text]);
1553
- const text = props.text;
1554
- const ms = props.ms;
1555
- try {
1556
- TextResource.read([text, ms]);
1557
- Scheduler.unstable_yieldValue(text);
1558
- return <span prop={text} />;
1559
- } catch (promise) {
1560
- if (typeof promise.then === 'function') {
1561
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1562
- } else {
1563
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
1895
}
1565
- throw promise;
1566
- }
1567
- }
1568
-
1569
- function App({text}) {
1570
- return (
1571
- <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1572
- <TextWithLifecycle text="A" />
1573
- <AsyncTextWithLifecycle ms={100} text={text} />
1574
- <TextWithLifecycle text="C" />
1575
- </Suspense>
1576
- );
1577
- }
1578
-
1579
- ReactNoop.renderLegacySyncRoot(<App text="B" />, () =>
1580
- Scheduler.unstable_yieldValue('Commit root'),
1581
- );
1582
- expect(Scheduler).toHaveYielded([
1583
- 'A',
1584
- 'Suspend! [B]',
1585
- 'C',
1586
- 'Loading...',
1587
-
1588
- 'Layout Effect [A]',
1589
- // B's effect should not fire because it suspended
1590
- // 'Layout Effect [B]',
1591
- 'Layout Effect [C]',
1592
- 'Layout Effect [Loading...]',
1593
- 'Commit root',
1594
- ]);
1595
-
1596
- // Flush passive effects.
1597
- expect(Scheduler).toFlushAndYield([
1598
- 'Effect [A]',
1599
- // B's effect should not fire because it suspended
1600
- // 'Effect [B]',
1601
- 'Effect [C]',
1602
- 'Effect [Loading...]',
1603
- ]);
1604
-
1605
- expect(ReactNoop).toMatchRenderedOutput(
1606
- <>
1607
- <span hidden={true} prop="A" />
1608
- <span hidden={true} prop="C" />
1609
- <span prop="Loading..." />
1610
- </>,
1611
- );
1612
-
1613
- Scheduler.unstable_advanceTime(500);
1614
- await advanceTimers(500);
1615
-
1616
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1617
-
1618
- expect(Scheduler).toFlushAndYield([
1619
- 'B',
1620
- 'Destroy Layout Effect [Loading...]',
1621
- 'Layout Effect [B]',
1622
- 'Destroy Effect [Loading...]',
1623
- 'Effect [B]',
1624
- ]);
1625
-
1626
- // Update
1627
- ReactNoop.renderLegacySyncRoot(<App text="B2" />, () =>
1628
- Scheduler.unstable_yieldValue('Commit root'),
1629
- );
1630
-
1631
- expect(Scheduler).toHaveYielded([
1632
- 'A',
1633
- 'Suspend! [B2]',
1634
- 'C',
1635
- 'Loading...',
1636
-
1637
- // B2's effect should not fire because it suspended
1638
- // 'Layout Effect [B2]',
1639
- 'Layout Effect [Loading...]',
1640
- 'Commit root',
1641
- ]);
1642
-
1643
- // Flush passive effects.
1644
- expect(Scheduler).toFlushAndYield([
1645
- // B2's effect should not fire because it suspended
1646
- // 'Effect [B2]',
1647
- 'Effect [Loading...]',
1648
- ]);
1649
-
1650
- Scheduler.unstable_advanceTime(500);
1651
- await advanceTimers(500);
1652
-
1653
- expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
1654
-
1655
- expect(Scheduler).toFlushAndYield([
1656
- 'B2',
1657
- 'Destroy Layout Effect [Loading...]',
1658
- 'Destroy Layout Effect [B]',
1659
- 'Layout Effect [B2]',
1660
- 'Destroy Effect [Loading...]',
1661
- 'Destroy Effect [B]',
1662
- 'Effect [B2]',
1663
- ]);
1664
- });
1896
1666
- it('suspends for longer if something took a long (CPU bound) time to render', async () => {
1667
- function Foo({renderContent}) {
1668
- Scheduler.unstable_yieldValue('Foo');
1669
- return (
1670
- <Suspense fallback={<Text text="Loading..." />}>
1671
- {renderContent ? <AsyncText text="A" ms={5000} /> : null}
1672
- </Suspense>
1673
- );
1674
- }
1675
-
1676
- ReactNoop.render(<Foo />);
1677
- expect(Scheduler).toFlushAndYield(['Foo']);
1678
-
1679
- ReactNoop.render(<Foo renderContent={true} />);
1680
- Scheduler.unstable_advanceTime(100);
1681
- await advanceTimers(100);
1682
- // Start rendering
1683
- expect(Scheduler).toFlushAndYieldThrough(['Foo']);
1684
- // For some reason it took a long time to render Foo.
1685
- Scheduler.unstable_advanceTime(1250);
1686
- await advanceTimers(1250);
1687
- expect(Scheduler).toFlushAndYield([
1688
- // A suspends
1689
- 'Suspend! [A]',
1690
- 'Loading...',
1691
- ]);
1692
- // We're now suspended and we haven't shown anything yet.
1693
- expect(ReactNoop.getChildren()).toEqual([]);
1694
-
1695
- // Flush some of the time
1696
- Scheduler.unstable_advanceTime(450);
1697
- await advanceTimers(450);
1698
- // Because we've already been waiting for so long we can
1699
- // wait a bit longer. Still nothing...
1700
- expect(Scheduler).toFlushWithoutYielding();
1701
- expect(ReactNoop.getChildren()).toEqual([]);
1702
-
1703
- // Eventually we'll show the fallback.
1704
- Scheduler.unstable_advanceTime(500);
1705
- await advanceTimers(500);
1706
- // No need to rerender.
1707
- expect(Scheduler).toFlushWithoutYielding();
1708
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1709
-
1710
- // Flush the promise completely
1711
- Scheduler.unstable_advanceTime(4500);
1712
- await advanceTimers(4500);
1713
- // Renders successfully
1714
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1715
- expect(Scheduler).toFlushAndYield(['A']);
1716
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
1717
- });
1897
+ ReactNoop.render(<Foo />);
1898
+ expect(Scheduler).toFlushAndYield(['Foo']);
1899
1719
- it('does not suspends if a fallback has been shown for a long time', async () => {
1720
- function Foo() {
1721
- Scheduler.unstable_yieldValue('Foo');
1722
- return (
1723
- <Suspense fallback={<Text text="Loading..." />}>
1724
- <AsyncText text="A" ms={5000} />
1725
- <Suspense fallback={<Text text="Loading more..." />}>
1726
- <AsyncText text="B" ms={10000} />
1727
- </Suspense>
1728
- </Suspense>
1729
- );
1730
- }
1731
-
1732
- ReactNoop.render(<Foo />);
1733
- // Start rendering
1734
- expect(Scheduler).toFlushAndYield([
1735
- 'Foo',
1736
- // A suspends
1737
- 'Suspend! [A]',
1738
- // B suspends
1739
- 'Suspend! [B]',
1740
- 'Loading more...',
1741
- 'Loading...',
1742
- ]);
1743
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1744
-
1745
- // Wait a long time.
1746
- Scheduler.unstable_advanceTime(5000);
1747
- await advanceTimers(5000);
1748
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1749
-
1750
- // Retry with the new content.
1751
- expect(Scheduler).toFlushAndYield([
1752
- 'A',
1753
- // B still suspends
1754
- 'Suspend! [B]',
1755
- 'Loading more...',
1756
- ]);
1757
- // Because we've already been waiting for so long we've exceeded
1758
- // our threshold and we show the next level immediately.
1759
- expect(ReactNoop.getChildren()).toEqual([
1760
- span('A'),
1761
- span('Loading more...'),
1762
- ]);
1763
-
1764
- // Flush the last promise completely
1765
- Scheduler.unstable_advanceTime(5000);
1766
- await advanceTimers(5000);
1767
- // Renders successfully
1768
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1769
- expect(Scheduler).toFlushAndYield(['B']);
1770
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1771
- });
1900
+ ReactNoop.discreteUpdates(() =>
1901
+ ReactNoop.render(<Foo renderContent={true} />),
1902
+ );
1903
+ expect(Scheduler).toFlushAndYieldThrough(['Foo']);
1904
1773
- it('does suspend if a fallback has been shown for a short time', async () => {
1774
- function Foo() {
1775
- Scheduler.unstable_yieldValue('Foo');
1776
- return (
1777
- <Suspense fallback={<Text text="Loading..." />}>
1778
- <AsyncText text="A" ms={200} />
1779
- <Suspense fallback={<Text text="Loading more..." />}>
1780
- <AsyncText text="B" ms={450} />
1781
- </Suspense>
1782
- </Suspense>
1783
- );
1784
- }
1785
-
1786
- ReactNoop.render(<Foo />);
1787
- // Start rendering
1788
- expect(Scheduler).toFlushAndYield([
1789
- 'Foo',
1790
- // A suspends
1791
- 'Suspend! [A]',
1792
- // B suspends
1793
- 'Suspend! [B]',
1794
- 'Loading more...',
1795
- 'Loading...',
1796
- ]);
1797
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1798
-
1799
- // Wait a short time.
1800
- Scheduler.unstable_advanceTime(250);
1801
- await advanceTimers(250);
1802
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1803
-
1804
- // Retry with the new content.
1805
- expect(Scheduler).toFlushAndYield([
1806
- 'A',
1807
- // B still suspends
1808
- 'Suspend! [B]',
1809
- 'Loading more...',
1810
- ]);
1811
- // Because we've already been waiting for so long we can
1812
- // wait a bit longer. Still nothing...
1813
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1814
-
1815
- Scheduler.unstable_advanceTime(200);
1816
- await advanceTimers(200);
1817
-
1818
- // Before we commit another Promise resolves.
1819
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1820
- // We're still showing the first loading state.
1821
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1822
- // Restart and render the complete content.
1823
- expect(Scheduler).toFlushAndYield(['A', 'B']);
1824
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1825
- });
1905
+ // Advance some time.
1906
+ Scheduler.unstable_advanceTime(100);
1907
+ await advanceTimers(100);
1908
1827
- it('does not suspend for very long after a higher priority update', async () => {
1828
- function Foo({renderContent}) {
1829
- Scheduler.unstable_yieldValue('Foo');
1830
- return (
1831
- <Suspense fallback={<Text text="Loading..." />}>
1832
- {renderContent ? <AsyncText text="A" ms={5000} /> : null}
1833
- </Suspense>
1834
- );
1835
- }
1836
-
1837
- ReactNoop.render(<Foo />);
1838
- expect(Scheduler).toFlushAndYield(['Foo']);
1839
-
1840
- ReactNoop.discreteUpdates(() =>
1841
- ReactNoop.render(<Foo renderContent={true} />),
1842
- );
1843
- expect(Scheduler).toFlushAndYieldThrough(['Foo']);
1844
-
1845
- // Advance some time.
1846
- Scheduler.unstable_advanceTime(100);
1847
- await advanceTimers(100);
1848
-
1849
- expect(Scheduler).toFlushAndYield([
1850
- // A suspends
1851
- 'Suspend! [A]',
1852
- 'Loading...',
1853
- ]);
1854
-
1855
- // We're now suspended and we haven't shown anything yet.
1856
- expect(ReactNoop.getChildren()).toEqual([]);
1857
-
1858
- // Flush some of the time
1859
- Scheduler.unstable_advanceTime(500);
1860
- jest.advanceTimersByTime(500);
1861
-
1862
- // We should have already shown the fallback.
1863
- // When we wrote this test, we inferred the start time of high priority
1864
- // updates as way earlier in the past. This test ensures that we don't
1865
- // use this assumption to add a very long JND.
1866
- expect(Scheduler).toFlushWithoutYielding();
1867
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1868
- });
1909
+ expect(Scheduler).toFlushAndYield([
1910
+ // A suspends
1911
+ 'Suspend! [A]',
1912
+ 'Loading...',
1913
+ ]);
1914
1870
- // TODO: flip to "warns" when this is implemented again.
1871
- it('does not warn when a low priority update suspends inside a high priority update for functional components', async () => {
1872
- let _setShow;
1873
- function App() {
1874
- let [show, setShow] = React.useState(false);
1875
- _setShow = setShow;
1876
- return (
1877
- <Suspense fallback="Loading...">
1878
- {show && <AsyncText text="A" />}
1879
- </Suspense>
1880
- );
1881
- }
1915
+ // We're now suspended and we haven't shown anything yet.
1916
+ expect(ReactNoop.getChildren()).toEqual([]);
1917
1883
- await ReactNoop.act(async () => {
1884
- ReactNoop.render(<App />);
1885
- });
1918
+ // Flush some of the time
1919
+ Scheduler.unstable_advanceTime(500);
1920
+ jest.advanceTimersByTime(500);
1921
1887
- // TODO: assert toErrorDev() when the warning is implemented again.
1888
- ReactNoop.act(() => {
1889
- Scheduler.unstable_runWithPriority(
1890
- Scheduler.unstable_UserBlockingPriority,
1891
- () => _setShow(true),
1892
- );
1893
- });
1894
- });
1922
+ // We should have already shown the fallback.
1923
+ // When we wrote this test, we inferred the start time of high priority
1924
+ // updates as way earlier in the past. This test ensures that we don't
1925
+ // use this assumption to add a very long JND.
1926
+ expect(Scheduler).toFlushWithoutYielding();
1927
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1928
+ });
1929
1896
- // TODO: flip to "warns" when this is implemented again.
1897
- it('does not warn when a low priority update suspends inside a high priority update for class components', async () => {
1898
- let show;
1899
- class App extends React.Component {
1900
- state = {show: false};
1901
-
1902
- render() {
1903
- show = () => this.setState({show: true});
1904
- return (
1905
- <Suspense fallback="Loading...">
1906
- {this.state.show && <AsyncText text="A" />}
1907
- </Suspense>
1908
- );
1909
- }
1910
- }
1930
+ // TODO: flip to "warns" when this is implemented again.
1931
+ it('does not warn when a low priority update suspends inside a high priority update for functional components', async () => {
1932
+ let _setShow;
1933
+ function App() {
1934
+ let [show, setShow] = React.useState(false);
1935
+ _setShow = setShow;
1936
+ return (
1937
+ <Suspense fallback="Loading...">
1938
+ {show && <AsyncText text="A" />}
1939
+ </Suspense>
1940
+ );
1941
+ }
1942
1912
- await ReactNoop.act(async () => {
1913
- ReactNoop.render(<App />);
1914
- });
1943
+ await ReactNoop.act(async () => {
1944
+ ReactNoop.render(<App />);
1945
+ });
1946
1916
- // TODO: assert toErrorDev() when the warning is implemented again.
1917
- ReactNoop.act(() => {
1918
- Scheduler.unstable_runWithPriority(
1919
- Scheduler.unstable_UserBlockingPriority,
1920
- () => show(),
1921
- );
1922
- });
1923
- });
1947
+ // TODO: assert toErrorDev() when the warning is implemented again.
1948
+ ReactNoop.act(() => {
1949
+ Scheduler.unstable_runWithPriority(
1950
+ Scheduler.unstable_UserBlockingPriority,
1951
+ () => _setShow(true),
1952
+ );
1953
+ });
1954
+ });
1955
1925
- it('does not warn about wrong Suspense priority if no new fallbacks are shown', async () => {
1926
- let showB;
1927
- class App extends React.Component {
1928
- state = {showB: false};
1929
-
1930
- render() {
1931
- showB = () => this.setState({showB: true});
1932
- return (
1933
- <Suspense fallback="Loading...">
1934
- {<AsyncText text="A" />}
1935
- {this.state.showB && <AsyncText text="B" />}
1936
- </Suspense>
1937
- );
1938
- }
1939
- }
1956
+ // TODO: flip to "warns" when this is implemented again.
1957
+ it('does not warn when a low priority update suspends inside a high priority update for class components', async () => {
1958
+ let show;
1959
+ class App extends React.Component {
1960
+ state = {show: false};
1961
+
1962
+ render() {
1963
+ show = () => this.setState({show: true});
1964
+ return (
1965
+ <Suspense fallback="Loading...">
1966
+ {this.state.show && <AsyncText text="A" />}
1967
+ </Suspense>
1968
+ );
1969
+ }
1970
+ }
1971
1941
- await ReactNoop.act(async () => {
1942
- ReactNoop.render(<App />);
1943
- });
1972
+ await ReactNoop.act(async () => {
1973
+ ReactNoop.render(<App />);
1974
+ });
1975
1945
- expect(Scheduler).toHaveYielded(['Suspend! [A]']);
1946
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
1976
+ // TODO: assert toErrorDev() when the warning is implemented again.
1977
+ ReactNoop.act(() => {
1978
+ Scheduler.unstable_runWithPriority(
1979
+ Scheduler.unstable_UserBlockingPriority,
1980
+ () => show(),
1981
+ );
1982
+ });
1983
+ });
1984
1948
- ReactNoop.act(() => {
1949
- Scheduler.unstable_runWithPriority(
1950
- Scheduler.unstable_UserBlockingPriority,
1951
- () => showB(),
1952
- );
1953
- });
1985
+ it('does not warn about wrong Suspense priority if no new fallbacks are shown', async () => {
1986
+ let showB;
1987
+ class App extends React.Component {
1988
+ state = {showB: false};
1989
+
1990
+ render() {
1991
+ showB = () => this.setState({showB: true});
1992
+ return (
1993
+ <Suspense fallback="Loading...">
1994
+ {<AsyncText text="A" />}
1995
+ {this.state.showB && <AsyncText text="B" />}
1996
+ </Suspense>
1997
+ );
1998
+ }
1999
+ }
2000
1955
- expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Suspend! [B]']);
1956
- });
2001
+ await ReactNoop.act(async () => {
2002
+ ReactNoop.render(<App />);
2003
+ });
2004
1958
- // TODO: flip to "warns" when this is implemented again.
1959
- it(
1960
- 'does not warn when component that triggered user-blocking update is between Suspense boundary ' +
1961
- 'and component that suspended',
1962
- async () => {
1963
- let _setShow;
1964
- function A() {
1965
- const [show, setShow] = React.useState(false);
1966
- _setShow = setShow;
1967
- return show && <AsyncText text="A" />;
1968
- }
1969
- function App() {
1970
- return (
1971
- <Suspense fallback="Loading...">
1972
- <A />
1973
- </Suspense>
1974
- );
1975
- }
1976
- await ReactNoop.act(async () => {
1977
- ReactNoop.render(<App />);
1978
- });
2005
+ expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2006
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
2007
1980
- // TODO: assert toErrorDev() when the warning is implemented again.
1981
- ReactNoop.act(() => {
1982
- Scheduler.unstable_runWithPriority(
1983
- Scheduler.unstable_UserBlockingPriority,
1984
- () => _setShow(true),
1985
- );
2008
+ ReactNoop.act(() => {
2009
+ Scheduler.unstable_runWithPriority(
2010
+ Scheduler.unstable_UserBlockingPriority,
2011
+ () => showB(),
2012
+ );
2013
+ });
2014
+
2015
+ expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Suspend! [B]']);
2016
});
1987
- },
1988
- );
2017
1990
- it('normal priority updates suspending do not warn for class components', async () => {
1991
- let show;
1992
- class App extends React.Component {
1993
- state = {show: false};
1994
-
1995
- render() {
1996
- show = () => this.setState({show: true});
1997
- return (
1998
- <Suspense fallback="Loading...">
1999
- {this.state.show && <AsyncText text="A" />}
2000
- </Suspense>
2001
- );
2002
- }
2003
- }
2018
+ // TODO: flip to "warns" when this is implemented again.
2019
+ it(
2020
+ 'does not warn when component that triggered user-blocking update is between Suspense boundary ' +
2021
+ 'and component that suspended',
2022
+ async () => {
2023
+ let _setShow;
2024
+ function A() {
2025
+ const [show, setShow] = React.useState(false);
2026
+ _setShow = setShow;
2027
+ return show && <AsyncText text="A" />;
2028
+ }
2029
+ function App() {
2030
+ return (
2031
+ <Suspense fallback="Loading...">
2032
+ <A />
2033
+ </Suspense>
2034
+ );
2035
+ }
2036
+ await ReactNoop.act(async () => {
2037
+ ReactNoop.render(<App />);
2038
+ });
2039
+
2040
+ // TODO: assert toErrorDev() when the warning is implemented again.
2041
+ ReactNoop.act(() => {
2042
+ Scheduler.unstable_runWithPriority(
2043
+ Scheduler.unstable_UserBlockingPriority,
2044
+ () => _setShow(true),
2045
+ );
2046
+ });
2047
+ },
2048
+ );
2049
+
2050
+ it('normal priority updates suspending do not warn for class components', async () => {
2051
+ let show;
2052
+ class App extends React.Component {
2053
+ state = {show: false};
2054
+
2055
+ render() {
2056
+ show = () => this.setState({show: true});
2057
+ return (
2058
+ <Suspense fallback="Loading...">
2059
+ {this.state.show && <AsyncText text="A" />}
2060
+ </Suspense>
2061
+ );
2062
+ }
2063
+ }
2064
2005
- await ReactNoop.act(async () => {
2006
- ReactNoop.render(<App />);
2007
- });
2065
+ await ReactNoop.act(async () => {
2066
+ ReactNoop.render(<App />);
2067
+ });
2068
2009
- // also make sure lowpriority is okay
2010
- await ReactNoop.act(async () => show(true));
2069
+ // also make sure lowpriority is okay
2070
+ await ReactNoop.act(async () => show(true));
2071
2012
- expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2013
- Scheduler.unstable_advanceTime(100);
2014
- await advanceTimers(100);
2072
+ expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2073
+ Scheduler.unstable_advanceTime(100);
2074
+ await advanceTimers(100);
2075
2016
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2017
- });
2076
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2077
+ });
2078
2019
- it('normal priority updates suspending do not warn for functional components', async () => {
2020
- let _setShow;
2021
- function App() {
2022
- let [show, setShow] = React.useState(false);
2023
- _setShow = setShow;
2024
- return (
2025
- <Suspense fallback="Loading...">
2026
- {show && <AsyncText text="A" />}
2027
- </Suspense>
2028
- );
2029
- }
2079
+ it('normal priority updates suspending do not warn for functional components', async () => {
2080
+ let _setShow;
2081
+ function App() {
2082
+ let [show, setShow] = React.useState(false);
2083
+ _setShow = setShow;
2084
+ return (
2085
+ <Suspense fallback="Loading...">
2086
+ {show && <AsyncText text="A" />}
2087
+ </Suspense>
2088
+ );
2089
+ }
2090
2031
- await ReactNoop.act(async () => {
2032
- ReactNoop.render(<App />);
2033
- });
2091
+ await ReactNoop.act(async () => {
2092
+ ReactNoop.render(<App />);
2093
+ });
2094
2035
- // also make sure lowpriority is okay
2036
- await ReactNoop.act(async () => _setShow(true));
2095
+ // also make sure lowpriority is okay
2096
+ await ReactNoop.act(async () => _setShow(true));
2097
2038
- expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2039
- Scheduler.unstable_advanceTime(100);
2040
- await advanceTimers(100);
2098
+ expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2099
+ Scheduler.unstable_advanceTime(100);
2100
+ await advanceTimers(100);
2101
2042
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2043
- });
2102
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2103
+ });
2104
2045
- it('shows the parent fallback if the inner fallback should be avoided', async () => {
2046
- function Foo({showC}) {
2047
- Scheduler.unstable_yieldValue('Foo');
2048
- return (
2049
- <Suspense fallback={<Text text="Initial load..." />}>
2050
- <Suspense
2051
- unstable_avoidThisFallback={true}
2052
- fallback={<Text text="Updating..." />}>
2053
- <AsyncText text="A" ms={5000} />
2054
- {showC ? <AsyncText text="C" ms={5000} /> : null}
2055
- </Suspense>
2056
- <Text text="B" />
2057
- </Suspense>
2058
- );
2059
- }
2060
-
2061
- ReactNoop.render(<Foo />);
2062
- expect(Scheduler).toFlushAndYield([
2063
- 'Foo',
2064
- 'Suspend! [A]',
2065
- 'B',
2066
- 'Initial load...',
2067
- ]);
2068
- expect(ReactNoop.getChildren()).toEqual([span('Initial load...')]);
2069
-
2070
- // Eventually we resolve and show the data.
2071
- Scheduler.unstable_advanceTime(5000);
2072
- await advanceTimers(5000);
2073
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2074
- expect(Scheduler).toFlushAndYield(['A', 'B']);
2075
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
2076
-
2077
- // Update to show C
2078
- ReactNoop.render(<Foo showC={true} />);
2079
- expect(Scheduler).toFlushAndYield([
2080
- 'Foo',
2081
- 'A',
2082
- 'Suspend! [C]',
2083
- 'Updating...',
2084
- 'B',
2085
- ]);
2086
- // Flush to skip suspended time.
2087
- Scheduler.unstable_advanceTime(600);
2088
- await advanceTimers(600);
2089
- // Since the optional suspense boundary is already showing its content,
2090
- // we have to use the inner fallback instead.
2091
- expect(ReactNoop.getChildren()).toEqual([
2092
- hiddenSpan('A'),
2093
- span('Updating...'),
2094
- span('B'),
2095
- ]);
2096
-
2097
- // Later we load the data.
2098
- Scheduler.unstable_advanceTime(5000);
2099
- await advanceTimers(5000);
2100
- expect(Scheduler).toHaveYielded(['Promise resolved [C]']);
2101
- expect(Scheduler).toFlushAndYield(['A', 'C']);
2102
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('C'), span('B')]);
2103
- });
2105
+ it('shows the parent fallback if the inner fallback should be avoided', async () => {
2106
+ function Foo({showC}) {
2107
+ Scheduler.unstable_yieldValue('Foo');
2108
+ return (
2109
+ <Suspense fallback={<Text text="Initial load..." />}>
2110
+ <Suspense
2111
+ unstable_avoidThisFallback={true}
2112
+ fallback={<Text text="Updating..." />}>
2113
+ <AsyncText text="A" ms={5000} />
2114
+ {showC ? <AsyncText text="C" ms={5000} /> : null}
2115
+ </Suspense>
2116
+ <Text text="B" />
2117
+ </Suspense>
2118
+ );
2119
+ }
2120
2105
- it('favors showing the inner fallback for nested top level avoided fallback', async () => {
2106
- function Foo({showB}) {
2107
- Scheduler.unstable_yieldValue('Foo');
2108
- return (
2109
- <Suspense
2110
- unstable_avoidThisFallback={true}
2111
- fallback={<Text text="Loading A..." />}>
2112
- <Text text="A" />
2113
- <Suspense
2114
- unstable_avoidThisFallback={true}
2115
- fallback={<Text text="Loading B..." />}>
2116
- <AsyncText text="B" ms={5000} />
2117
- </Suspense>
2118
- </Suspense>
2119
- );
2120
- }
2121
-
2122
- ReactNoop.render(<Foo />);
2123
- expect(Scheduler).toFlushAndYield([
2124
- 'Foo',
2125
- 'A',
2126
- 'Suspend! [B]',
2127
- 'Loading B...',
2128
- ]);
2129
- // Flush to skip suspended time.
2130
- Scheduler.unstable_advanceTime(600);
2131
- await advanceTimers(600);
2132
-
2133
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
2134
- });
2121
+ ReactNoop.render(<Foo />);
2122
+ expect(Scheduler).toFlushAndYield([
2123
+ 'Foo',
2124
+ 'Suspend! [A]',
2125
+ 'B',
2126
+ 'Initial load...',
2127
+ ]);
2128
+ expect(ReactNoop.getChildren()).toEqual([span('Initial load...')]);
2129
+
2130
+ // Eventually we resolve and show the data.
2131
+ Scheduler.unstable_advanceTime(5000);
2132
+ await advanceTimers(5000);
2133
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2134
+ expect(Scheduler).toFlushAndYield(['A', 'B']);
2135
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
2136
+
2137
+ // Update to show C
2138
+ ReactNoop.render(<Foo showC={true} />);
2139
+ expect(Scheduler).toFlushAndYield([
2140
+ 'Foo',
2141
+ 'A',
2142
+ 'Suspend! [C]',
2143
+ 'Updating...',
2144
+ 'B',
2145
+ ]);
2146
+ // Flush to skip suspended time.
2147
+ Scheduler.unstable_advanceTime(600);
2148
+ await advanceTimers(600);
2149
+ // Since the optional suspense boundary is already showing its content,
2150
+ // we have to use the inner fallback instead.
2151
+ expect(ReactNoop.getChildren()).toEqual([
2152
+ hiddenSpan('A'),
2153
+ span('Updating...'),
2154
+ span('B'),
2155
+ ]);
2156
2136
- it('keeps showing an avoided parent fallback if it is already showing', async () => {
2137
- function Foo({showB}) {
2138
- Scheduler.unstable_yieldValue('Foo');
2139
- return (
2140
- <Suspense fallback={<Text text="Initial load..." />}>
2141
- <Suspense
2142
- unstable_avoidThisFallback={true}
2143
- fallback={<Text text="Loading A..." />}>
2144
- <Text text="A" />
2145
- {showB ? (
2157
+ // Later we load the data.
2158
+ Scheduler.unstable_advanceTime(5000);
2159
+ await advanceTimers(5000);
2160
+ expect(Scheduler).toHaveYielded(['Promise resolved [C]']);
2161
+ expect(Scheduler).toFlushAndYield(['A', 'C']);
2162
+ expect(ReactNoop.getChildren()).toEqual([
2163
+ span('A'),
2164
+ span('C'),
2165
+ span('B'),
2166
+ ]);
2167
+ });
2168
+
2169
+ it('favors showing the inner fallback for nested top level avoided fallback', async () => {
2170
+ function Foo({showB}) {
2171
+ Scheduler.unstable_yieldValue('Foo');
2172
+ return (
2173
+ <Suspense
2174
+ unstable_avoidThisFallback={true}
2175
+ fallback={<Text text="Loading A..." />}>
2176
+ <Text text="A" />
2177
<Suspense
2178
unstable_avoidThisFallback={true}
2179
fallback={<Text text="Loading B..." />}>
2180
<AsyncText text="B" ms={5000} />
2181
</Suspense>
2151
- ) : null}
2152
- </Suspense>
2153
- </Suspense>
2154
- );
2155
- }
2182
+ </Suspense>
2183
+ );
2184
+ }
2185
+
2186
+ ReactNoop.render(<Foo />);
2187
+ expect(Scheduler).toFlushAndYield([
2188
+ 'Foo',
2189
+ 'A',
2190
+ 'Suspend! [B]',
2191
+ 'Loading B...',
2192
+ ]);
2193
+ // Flush to skip suspended time.
2194
+ Scheduler.unstable_advanceTime(600);
2195
+ await advanceTimers(600);
2196
+
2197
+ expect(ReactNoop.getChildren()).toEqual([
2198
+ span('A'),
2199
+ span('Loading B...'),
2200
+ ]);
2201
+ });
2202
+
2203
+ it('keeps showing an avoided parent fallback if it is already showing', async () => {
2204
+ function Foo({showB}) {
2205
+ Scheduler.unstable_yieldValue('Foo');
2206
+ return (
2207
+ <Suspense fallback={<Text text="Initial load..." />}>
2208
+ <Suspense
2209
+ unstable_avoidThisFallback={true}
2210
+ fallback={<Text text="Loading A..." />}>
2211
+ <Text text="A" />
2212
+ {showB ? (
2213
+ <Suspense
2214
+ unstable_avoidThisFallback={true}
2215
+ fallback={<Text text="Loading B..." />}>
2216
+ <AsyncText text="B" ms={5000} />
2217
+ </Suspense>
2218
+ ) : null}
2219
+ </Suspense>
2220
+ </Suspense>
2221
+ );
2222
+ }
2223
2157
- ReactNoop.render(<Foo />);
2158
- expect(Scheduler).toFlushAndYield(['Foo', 'A']);
2159
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2224
+ ReactNoop.render(<Foo />);
2225
+ expect(Scheduler).toFlushAndYield(['Foo', 'A']);
2226
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2227
2161
- ReactNoop.render(<Foo showB={true} />);
2228
+ ReactNoop.render(<Foo showB={true} />);
2229
2163
- expect(Scheduler).toFlushAndYield([
2164
- 'Foo',
2165
- 'A',
2166
- 'Suspend! [B]',
2167
- 'Loading B...',
2168
- ]);
2169
- // Still suspended.
2170
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2230
+ expect(Scheduler).toFlushAndYield([
2231
+ 'Foo',
2232
+ 'A',
2233
+ 'Suspend! [B]',
2234
+ 'Loading B...',
2235
+ ]);
2236
+ // Still suspended.
2237
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2238
2172
- // Flush to skip suspended time.
2173
- Scheduler.unstable_advanceTime(600);
2174
- await advanceTimers(600);
2239
+ // Flush to skip suspended time.
2240
+ Scheduler.unstable_advanceTime(600);
2241
+ await advanceTimers(600);
2242
2176
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
2177
- });
2243
+ expect(ReactNoop.getChildren()).toEqual([
2244
+ span('A'),
2245
+ span('Loading B...'),
2246
+ ]);
2247
+ });
2248
2179
- it('commits a suspended idle pri render within a reasonable time', async () => {
2180
- function Foo({renderContent}) {
2181
- return (
2182
- <Fragment>
2183
- <Suspense fallback={<Text text="Loading A..." />}>
2184
- {renderContent ? <AsyncText text="A" ms={10000} /> : null}
2185
- </Suspense>
2186
- </Fragment>
2187
- );
2188
- }
2249
+ it('commits a suspended idle pri render within a reasonable time', async () => {
2250
+ function Foo({renderContent}) {
2251
+ return (
2252
+ <Fragment>
2253
+ <Suspense fallback={<Text text="Loading A..." />}>
2254
+ {renderContent ? <AsyncText text="A" ms={10000} /> : null}
2255
+ </Suspense>
2256
+ </Fragment>
2257
+ );
2258
+ }
2259
2190
- ReactNoop.render(<Foo />);
2191
- expect(Scheduler).toFlushAndYield([]);
2260
+ ReactNoop.render(<Foo />);
2261
+ expect(Scheduler).toFlushAndYield([]);
2262
2193
- ReactNoop.render(<Foo renderContent={1} />);
2263
+ ReactNoop.render(<Foo renderContent={1} />);
2264
2195
- // Took a long time to render. This is to ensure we get a long suspense time.
2196
- // Could also use something like withSuspenseConfig to simulate this.
2197
- Scheduler.unstable_advanceTime(1500);
2198
- await advanceTimers(1500);
2265
+ // Took a long time to render. This is to ensure we get a long suspense time.
2266
+ // Could also use something like withSuspenseConfig to simulate this.
2267
+ Scheduler.unstable_advanceTime(1500);
2268
+ await advanceTimers(1500);
2269
2200
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading A...']);
2201
- // We're still suspended.
2202
- expect(ReactNoop.getChildren()).toEqual([]);
2270
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading A...']);
2271
+ // We're still suspended.
2272
+ expect(ReactNoop.getChildren()).toEqual([]);
2273
2204
- // Schedule an update at idle pri.
2205
- Scheduler.unstable_runWithPriority(Scheduler.unstable_IdlePriority, () =>
2206
- ReactNoop.render(<Foo renderContent={2} />),
2207
- );
2208
- // We won't even work on Idle priority.
2209
- expect(Scheduler).toFlushAndYield([]);
2274
+ // Schedule an update at idle pri.
2275
+ Scheduler.unstable_runWithPriority(
2276
+ Scheduler.unstable_IdlePriority,
2277
+ () => ReactNoop.render(<Foo renderContent={2} />),
2278
+ );
2279
+ // We won't even work on Idle priority.
2280
+ expect(Scheduler).toFlushAndYield([]);
2281
2211
- // We're still suspended.
2212
- expect(ReactNoop.getChildren()).toEqual([]);
2282
+ // We're still suspended.
2283
+ expect(ReactNoop.getChildren()).toEqual([]);
2284
2214
- // Advance time a little bit.
2215
- Scheduler.unstable_advanceTime(150);
2216
- await advanceTimers(150);
2285
+ // Advance time a little bit.
2286
+ Scheduler.unstable_advanceTime(150);
2287
+ await advanceTimers(150);
2288
2218
- // We should not have committed yet because we had a long suspense time.
2219
- expect(ReactNoop.getChildren()).toEqual([]);
2289
+ // We should not have committed yet because we had a long suspense time.
2290
+ expect(ReactNoop.getChildren()).toEqual([]);
2291
2221
- // Flush to skip suspended time.
2222
- Scheduler.unstable_advanceTime(600);
2223
- await advanceTimers(600);
2292
+ // Flush to skip suspended time.
2293
+ Scheduler.unstable_advanceTime(600);
2294
+ await advanceTimers(600);
2295
2225
- expect(ReactNoop.getChildren()).toEqual([span('Loading A...')]);
2226
- });
2296
+ expect(ReactNoop.getChildren()).toEqual([span('Loading A...')]);
2297
+ });
2298
2228
- describe('delays transitions when there a suspense config is supplied', () => {
2229
- const SUSPENSE_CONFIG = {
2230
- timeoutMs: 2000,
2231
- };
2299
+ describe('delays transitions when there a suspense config is supplied', () => {
2300
+ const SUSPENSE_CONFIG = {
2301
+ timeoutMs: 2000,
2302
+ };
2303
2233
- it('top level render', async () => {
2234
- function App({page}) {
2235
- return (
2236
- <Suspense fallback={<Text text="Loading..." />}>
2237
- <AsyncText text={page} ms={5000} />
2238
- </Suspense>
2239
- );
2240
- }
2304
+ it('top level render', async () => {
2305
+ function App({page}) {
2306
+ return (
2307
+ <Suspense fallback={<Text text="Loading..." />}>
2308
+ <AsyncText text={page} ms={5000} />
2309
+ </Suspense>
2310
+ );
2311
+ }
2312
2242
- // Initial render.
2243
- React.unstable_withSuspenseConfig(
2244
- () => ReactNoop.render(<App page="A" />),
2245
- SUSPENSE_CONFIG,
2246
- );
2313
+ // Initial render.
2314
+ React.unstable_withSuspenseConfig(
2315
+ () => ReactNoop.render(<App page="A" />),
2316
+ SUSPENSE_CONFIG,
2317
+ );
2318
2248
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2249
- // Only a short time is needed to unsuspend the initial loading state.
2250
- Scheduler.unstable_advanceTime(400);
2251
- await advanceTimers(400);
2252
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2253
-
2254
- // Later we load the data.
2255
- Scheduler.unstable_advanceTime(5000);
2256
- await advanceTimers(5000);
2257
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2258
- expect(Scheduler).toFlushAndYield(['A']);
2259
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2260
-
2261
- // Start transition.
2262
- React.unstable_withSuspenseConfig(
2263
- () => ReactNoop.render(<App page="B" />),
2264
- SUSPENSE_CONFIG,
2265
- );
2319
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2320
+ // Only a short time is needed to unsuspend the initial loading state.
2321
+ Scheduler.unstable_advanceTime(400);
2322
+ await advanceTimers(400);
2323
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2324
+
2325
+ // Later we load the data.
2326
+ Scheduler.unstable_advanceTime(5000);
2327
+ await advanceTimers(5000);
2328
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2329
+ expect(Scheduler).toFlushAndYield(['A']);
2330
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2331
+
2332
+ // Start transition.
2333
+ React.unstable_withSuspenseConfig(
2334
+ () => ReactNoop.render(<App page="B" />),
2335
+ SUSPENSE_CONFIG,
2336
+ );
2337
2267
- expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2268
- Scheduler.unstable_advanceTime(1000);
2269
- await advanceTimers(1000);
2270
- // Even after a second, we have still not yet flushed the loading state.
2271
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2272
- Scheduler.unstable_advanceTime(1100);
2273
- await advanceTimers(1100);
2274
- // After the timeout, we do show the loading state.
2275
- expect(ReactNoop.getChildren()).toEqual([
2276
- hiddenSpan('A'),
2277
- span('Loading...'),
2278
- ]);
2279
- // Later we load the data.
2280
- Scheduler.unstable_advanceTime(3000);
2281
- await advanceTimers(3000);
2282
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2283
- expect(Scheduler).toFlushAndYield(['B']);
2284
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2285
- });
2338
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2339
+ Scheduler.unstable_advanceTime(1000);
2340
+ await advanceTimers(1000);
2341
+ // Even after a second, we have still not yet flushed the loading state.
2342
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2343
+ Scheduler.unstable_advanceTime(1100);
2344
+ await advanceTimers(1100);
2345
+ // After the timeout, we do show the loading state.
2346
+ expect(ReactNoop.getChildren()).toEqual([
2347
+ hiddenSpan('A'),
2348
+ span('Loading...'),
2349
+ ]);
2350
+ // Later we load the data.
2351
+ Scheduler.unstable_advanceTime(3000);
2352
+ await advanceTimers(3000);
2353
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2354
+ expect(Scheduler).toFlushAndYield(['B']);
2355
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2356
+ });
2357
+
2358
+ it('hooks', async () => {
2359
+ let transitionToPage;
2360
+ function App() {
2361
+ let [page, setPage] = React.useState('none');
2362
+ transitionToPage = setPage;
2363
+ if (page === 'none') {
2364
+ return null;
2365
+ }
2366
+ return (
2367
+ <Suspense fallback={<Text text="Loading..." />}>
2368
+ <AsyncText text={page} ms={5000} />
2369
+ </Suspense>
2370
+ );
2371
+ }
2372
2287
- it('hooks', async () => {
2288
- let transitionToPage;
2289
- function App() {
2290
- let [page, setPage] = React.useState('none');
2291
- transitionToPage = setPage;
2292
- if (page === 'none') {
2293
- return null;
2294
- }
2295
- return (
2296
- <Suspense fallback={<Text text="Loading..." />}>
2297
- <AsyncText text={page} ms={5000} />
2298
- </Suspense>
2299
- );
2300
- }
2373
+ ReactNoop.render(<App />);
2374
+ expect(Scheduler).toFlushAndYield([]);
2375
2302
- ReactNoop.render(<App />);
2303
- expect(Scheduler).toFlushAndYield([]);
2376
+ // Initial render.
2377
+ await ReactNoop.act(async () => {
2378
+ React.unstable_withSuspenseConfig(
2379
+ () => transitionToPage('A'),
2380
+ SUSPENSE_CONFIG,
2381
+ );
2382
2305
- // Initial render.
2306
- await ReactNoop.act(async () => {
2307
- React.unstable_withSuspenseConfig(
2308
- () => transitionToPage('A'),
2309
- SUSPENSE_CONFIG,
2310
- );
2383
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2384
+ // Only a short time is needed to unsuspend the initial loading state.
2385
+ Scheduler.unstable_advanceTime(400);
2386
+ await advanceTimers(400);
2387
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2388
+ });
2389
2312
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2313
- // Only a short time is needed to unsuspend the initial loading state.
2314
- Scheduler.unstable_advanceTime(400);
2315
- await advanceTimers(400);
2316
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2390
+ // Later we load the data.
2391
+ Scheduler.unstable_advanceTime(5000);
2392
+ await advanceTimers(5000);
2393
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2394
+ expect(Scheduler).toFlushAndYield(['A']);
2395
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2396
+
2397
+ // Start transition.
2398
+ await ReactNoop.act(async () => {
2399
+ React.unstable_withSuspenseConfig(
2400
+ () => transitionToPage('B'),
2401
+ SUSPENSE_CONFIG,
2402
+ );
2403
+
2404
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2405
+ Scheduler.unstable_advanceTime(1000);
2406
+ await advanceTimers(1000);
2407
+ // Even after a second, we have still not yet flushed the loading state.
2408
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2409
+ Scheduler.unstable_advanceTime(1100);
2410
+ await advanceTimers(1100);
2411
+ // After the timeout, we do show the loading state.
2412
+ expect(ReactNoop.getChildren()).toEqual([
2413
+ hiddenSpan('A'),
2414
+ span('Loading...'),
2415
+ ]);
2416
+ });
2417
+ // Later we load the data.
2418
+ Scheduler.unstable_advanceTime(3000);
2419
+ await advanceTimers(3000);
2420
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2421
+ expect(Scheduler).toFlushAndYield(['B']);
2422
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2423
+ });
2424
+
2425
+ it('classes', async () => {
2426
+ let transitionToPage;
2427
+ class App extends React.Component {
2428
+ state = {page: 'none'};
2429
+ render() {
2430
+ transitionToPage = page => this.setState({page});
2431
+ let page = this.state.page;
2432
+ if (page === 'none') {
2433
+ return null;
2434
+ }
2435
+ return (
2436
+ <Suspense fallback={<Text text="Loading..." />}>
2437
+ <AsyncText text={page} ms={5000} />
2438
+ </Suspense>
2439
+ );
2440
+ }
2441
+ }
2442
+
2443
+ ReactNoop.render(<App />);
2444
+ expect(Scheduler).toFlushAndYield([]);
2445
+
2446
+ // Initial render.
2447
+ await ReactNoop.act(async () => {
2448
+ React.unstable_withSuspenseConfig(
2449
+ () => transitionToPage('A'),
2450
+ SUSPENSE_CONFIG,
2451
+ );
2452
+
2453
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2454
+ // Only a short time is needed to unsuspend the initial loading state.
2455
+ Scheduler.unstable_advanceTime(400);
2456
+ await advanceTimers(400);
2457
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2458
+ });
2459
+
2460
+ // Later we load the data.
2461
+ Scheduler.unstable_advanceTime(5000);
2462
+ await advanceTimers(5000);
2463
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2464
+ expect(Scheduler).toFlushAndYield(['A']);
2465
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2466
+
2467
+ // Start transition.
2468
+ await ReactNoop.act(async () => {
2469
+ React.unstable_withSuspenseConfig(
2470
+ () => transitionToPage('B'),
2471
+ SUSPENSE_CONFIG,
2472
+ );
2473
+
2474
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2475
+ Scheduler.unstable_advanceTime(1000);
2476
+ await advanceTimers(1000);
2477
+ // Even after a second, we have still not yet flushed the loading state.
2478
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2479
+ Scheduler.unstable_advanceTime(1100);
2480
+ await advanceTimers(1100);
2481
+ // After the timeout, we do show the loading state.
2482
+ expect(ReactNoop.getChildren()).toEqual([
2483
+ hiddenSpan('A'),
2484
+ span('Loading...'),
2485
+ ]);
2486
+ });
2487
+ // Later we load the data.
2488
+ Scheduler.unstable_advanceTime(3000);
2489
+ await advanceTimers(3000);
2490
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2491
+ expect(Scheduler).toFlushAndYield(['B']);
2492
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2493
+ });
2494
});
2495
2319
- // Later we load the data.
2320
- Scheduler.unstable_advanceTime(5000);
2321
- await advanceTimers(5000);
2322
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2323
- expect(Scheduler).toFlushAndYield(['A']);
2324
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2496
+ it('disables suspense config when nothing is passed to withSuspenseConfig', async () => {
2497
+ function App({page}) {
2498
+ return (
2499
+ <Suspense fallback={<Text text="Loading..." />}>
2500
+ <AsyncText text={page} ms={2000} />
2501
+ </Suspense>
2502
+ );
2503
+ }
2504
+
2505
+ // Initial render.
2506
+ ReactNoop.render(<App page="A" />);
2507
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2508
+ Scheduler.unstable_advanceTime(2000);
2509
+ await advanceTimers(2000);
2510
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2511
+ expect(Scheduler).toFlushAndYield(['A']);
2512
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2513
2326
- // Start transition.
2327
- await ReactNoop.act(async () => {
2514
+ // Start transition.
2515
React.unstable_withSuspenseConfig(
2329
- () => transitionToPage('B'),
2330
- SUSPENSE_CONFIG,
2516
+ () => {
2517
+ // When we schedule an inner transition without a suspense config
2518
+ // so it should only suspend for a short time.
2519
+ React.unstable_withSuspenseConfig(() =>
2520
+ ReactNoop.render(<App page="B" />),
2521
+ );
2522
+ },
2523
+ {timeoutMs: 2000},
2524
);
2525
2526
expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2334
- Scheduler.unstable_advanceTime(1000);
2335
- await advanceTimers(1000);
2336
- // Even after a second, we have still not yet flushed the loading state.
2527
+ // Suspended
2528
expect(ReactNoop.getChildren()).toEqual([span('A')]);
2338
- Scheduler.unstable_advanceTime(1100);
2339
- await advanceTimers(1100);
2340
- // After the timeout, we do show the loading state.
2529
+ Scheduler.unstable_advanceTime(500);
2530
+ await advanceTimers(500);
2531
+ // Committed loading state.
2532
expect(ReactNoop.getChildren()).toEqual([
2533
hiddenSpan('A'),
2534
span('Loading...'),
2535
]);
2536
+
2537
+ Scheduler.unstable_advanceTime(2000);
2538
+ await advanceTimers(2000);
2539
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2540
+ expect(Scheduler).toFlushAndYield(['B']);
2541
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2542
+
2543
+ React.unstable_withSuspenseConfig(
2544
+ () => {
2545
+ // First we schedule an inner unrelated update.
2546
+ React.unstable_withSuspenseConfig(() =>
2547
+ ReactNoop.render(<App page="B" unrelated={true} />),
2548
+ );
2549
+ // Then we schedule another transition to a slow page,
2550
+ // but at this scope we should suspend for longer.
2551
+ Scheduler.unstable_next(() => ReactNoop.render(<App page="C" />));
2552
+ },
2553
+ {timeoutMs: 2000},
2554
+ );
2555
+ expect(Scheduler).toFlushAndYield([
2556
+ 'Suspend! [C]',
2557
+ 'Loading...',
2558
+ 'Suspend! [C]',
2559
+ 'Loading...',
2560
+ ]);
2561
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2562
+ Scheduler.unstable_advanceTime(1200);
2563
+ await advanceTimers(1200);
2564
+ // Even after a second, we have still not yet flushed the loading state.
2565
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2566
+ Scheduler.unstable_advanceTime(1200);
2567
+ await advanceTimers(1200);
2568
+ // After the two second timeout we show the loading state.
2569
+ expect(ReactNoop.getChildren()).toEqual([
2570
+ hiddenSpan('B'),
2571
+ span('Loading...'),
2572
+ ]);
2573
});
2346
- // Later we load the data.
2347
- Scheduler.unstable_advanceTime(3000);
2348
- await advanceTimers(3000);
2349
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2350
- expect(Scheduler).toFlushAndYield(['B']);
2351
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2352
- });
2574
2354
- it('classes', async () => {
2355
- let transitionToPage;
2356
- class App extends React.Component {
2357
- state = {page: 'none'};
2358
- render() {
2359
- transitionToPage = page => this.setState({page});
2360
- let page = this.state.page;
2361
- if (page === 'none') {
2362
- return null;
2363
- }
2575
+ it('withSuspenseConfig timeout applies when we use an updated avoided boundary', async () => {
2576
+ function App({page}) {
2577
return (
2578
<Suspense fallback={<Text text="Loading..." />}>
2366
- <AsyncText text={page} ms={5000} />
2579
+ <Text text="Hi!" />
2580
+ <Suspense
2581
+ fallback={<Text text={'Loading ' + page + '...'} />}
2582
+ unstable_avoidThisFallback={true}>
2583
+ <AsyncText text={page} ms={3000} />
2584
+ </Suspense>
2585
</Suspense>
2586
);
2587
}
2370
- }
2588
2372
- ReactNoop.render(<App />);
2373
- expect(Scheduler).toFlushAndYield([]);
2589
+ // Initial render.
2590
+ ReactNoop.render(<App page="A" />);
2591
+ expect(Scheduler).toFlushAndYield([
2592
+ 'Hi!',
2593
+ 'Suspend! [A]',
2594
+ 'Loading...',
2595
+ ]);
2596
+ Scheduler.unstable_advanceTime(3000);
2597
+ await advanceTimers(3000);
2598
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2599
+ expect(Scheduler).toFlushAndYield(['Hi!', 'A']);
2600
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2601
2375
- // Initial render.
2376
- await ReactNoop.act(async () => {
2602
+ // Start transition.
2603
React.unstable_withSuspenseConfig(
2378
- () => transitionToPage('A'),
2379
- SUSPENSE_CONFIG,
2604
+ () => ReactNoop.render(<App page="B" />),
2605
+ {timeoutMs: 2000},
2606
);
2607
2382
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2383
- // Only a short time is needed to unsuspend the initial loading state.
2384
- Scheduler.unstable_advanceTime(400);
2385
- await advanceTimers(400);
2386
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2608
+ expect(Scheduler).toFlushAndYield([
2609
+ 'Hi!',
2610
+ 'Suspend! [B]',
2611
+ 'Loading B...',
2612
+ ]);
2613
+
2614
+ // Suspended
2615
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2616
+ Scheduler.unstable_advanceTime(1800);
2617
+ await advanceTimers(1800);
2618
+ expect(Scheduler).toFlushAndYield([]);
2619
+ // We should still be suspended here because this loading state should be avoided.
2620
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2621
+ Scheduler.unstable_advanceTime(1500);
2622
+ await advanceTimers(1500);
2623
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2624
+ expect(ReactNoop.getChildren()).toEqual([
2625
+ span('Hi!'),
2626
+ hiddenSpan('A'),
2627
+ span('Loading B...'),
2628
+ ]);
2629
});
2630
2389
- // Later we load the data.
2390
- Scheduler.unstable_advanceTime(5000);
2391
- await advanceTimers(5000);
2392
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2393
- expect(Scheduler).toFlushAndYield(['A']);
2394
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2631
+ it('withSuspenseConfig timeout applies when we use a newly created avoided boundary', async () => {
2632
+ function App({page}) {
2633
+ return (
2634
+ <Suspense fallback={<Text text="Loading..." />}>
2635
+ <Text text="Hi!" />
2636
+ {page === 'A' ? (
2637
+ <Text text="A" />
2638
+ ) : (
2639
+ <Suspense
2640
+ fallback={<Text text={'Loading ' + page + '...'} />}
2641
+ unstable_avoidThisFallback={true}>
2642
+ <AsyncText text={page} ms={3000} />
2643
+ </Suspense>
2644
+ )}
2645
+ </Suspense>
2646
+ );
2647
+ }
2648
+
2649
+ // Initial render.
2650
+ ReactNoop.render(<App page="A" />);
2651
+ expect(Scheduler).toFlushAndYield(['Hi!', 'A']);
2652
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2653
2396
- // Start transition.
2397
- await ReactNoop.act(async () => {
2654
+ // Start transition.
2655
React.unstable_withSuspenseConfig(
2399
- () => transitionToPage('B'),
2400
- SUSPENSE_CONFIG,
2656
+ () => ReactNoop.render(<App page="B" />),
2657
+ {timeoutMs: 2000},
2658
);
2659
2403
- expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2404
- Scheduler.unstable_advanceTime(1000);
2405
- await advanceTimers(1000);
2406
- // Even after a second, we have still not yet flushed the loading state.
2407
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2408
- Scheduler.unstable_advanceTime(1100);
2409
- await advanceTimers(1100);
2410
- // After the timeout, we do show the loading state.
2660
+ expect(Scheduler).toFlushAndYield([
2661
+ 'Hi!',
2662
+ 'Suspend! [B]',
2663
+ 'Loading B...',
2664
+ ]);
2665
+
2666
+ // Suspended
2667
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2668
+ Scheduler.unstable_advanceTime(1800);
2669
+ await advanceTimers(1800);
2670
+ expect(Scheduler).toFlushAndYield([]);
2671
+ // We should still be suspended here because this loading state should be avoided.
2672
+ expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2673
+ Scheduler.unstable_advanceTime(1500);
2674
+ await advanceTimers(1500);
2675
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2676
expect(ReactNoop.getChildren()).toEqual([
2412
- hiddenSpan('A'),
2413
- span('Loading...'),
2677
+ span('Hi!'),
2678
+ span('Loading B...'),
2679
]);
2680
});
2416
- // Later we load the data.
2417
- Scheduler.unstable_advanceTime(3000);
2418
- await advanceTimers(3000);
2419
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2420
- expect(Scheduler).toFlushAndYield(['B']);
2421
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2422
- });
2423
- });
2681
2425
- it('disables suspense config when nothing is passed to withSuspenseConfig', async () => {
2426
- function App({page}) {
2427
- return (
2428
- <Suspense fallback={<Text text="Loading..." />}>
2429
- <AsyncText text={page} ms={2000} />
2430
- </Suspense>
2431
- );
2432
- }
2433
-
2434
- // Initial render.
2435
- ReactNoop.render(<App page="A" />);
2436
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2437
- Scheduler.unstable_advanceTime(2000);
2438
- await advanceTimers(2000);
2439
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2440
- expect(Scheduler).toFlushAndYield(['A']);
2441
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2442
-
2443
- // Start transition.
2444
- React.unstable_withSuspenseConfig(
2445
- () => {
2446
- // When we schedule an inner transition without a suspense config
2447
- // so it should only suspend for a short time.
2448
- React.unstable_withSuspenseConfig(() =>
2449
- ReactNoop.render(<App page="B" />),
2450
- );
2451
- },
2452
- {timeoutMs: 2000},
2453
- );
2454
-
2455
- expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2456
- // Suspended
2457
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2458
- Scheduler.unstable_advanceTime(500);
2459
- await advanceTimers(500);
2460
- // Committed loading state.
2461
- expect(ReactNoop.getChildren()).toEqual([
2462
- hiddenSpan('A'),
2463
- span('Loading...'),
2464
- ]);
2465
-
2466
- Scheduler.unstable_advanceTime(2000);
2467
- await advanceTimers(2000);
2468
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2469
- expect(Scheduler).toFlushAndYield(['B']);
2470
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2471
-
2472
- React.unstable_withSuspenseConfig(
2473
- () => {
2474
- // First we schedule an inner unrelated update.
2475
- React.unstable_withSuspenseConfig(() =>
2476
- ReactNoop.render(<App page="B" unrelated={true} />),
2477
- );
2478
- // Then we schedule another transition to a slow page,
2479
- // but at this scope we should suspend for longer.
2480
- Scheduler.unstable_next(() => ReactNoop.render(<App page="C" />));
2481
- },
2482
- {timeoutMs: 2000},
2483
- );
2484
- expect(Scheduler).toFlushAndYield([
2485
- 'Suspend! [C]',
2486
- 'Loading...',
2487
- 'Suspend! [C]',
2488
- 'Loading...',
2489
- ]);
2490
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2491
- Scheduler.unstable_advanceTime(1200);
2492
- await advanceTimers(1200);
2493
- // Even after a second, we have still not yet flushed the loading state.
2494
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2495
- Scheduler.unstable_advanceTime(1200);
2496
- await advanceTimers(1200);
2497
- // After the two second timeout we show the loading state.
2498
- expect(ReactNoop.getChildren()).toEqual([
2499
- hiddenSpan('B'),
2500
- span('Loading...'),
2501
- ]);
2502
- });
2682
+ it('supports delaying a busy spinner from disappearing', async () => {
2683
+ function useLoadingIndicator(config) {
2684
+ let [isLoading, setLoading] = React.useState(false);
2685
+ let start = React.useCallback(
2686
+ cb => {
2687
+ setLoading(true);
2688
+ Scheduler.unstable_next(() =>
2689
+ React.unstable_withSuspenseConfig(() => {
2690
+ setLoading(false);
2691
+ cb();
2692
+ }, config),
2693
+ );
2694
+ },
2695
+ [setLoading, config],
2696
+ );
2697
+ return [isLoading, start];
2698
+ }
2699
2504
- it('withSuspenseConfig timeout applies when we use an updated avoided boundary', async () => {
2505
- function App({page}) {
2506
- return (
2507
- <Suspense fallback={<Text text="Loading..." />}>
2508
- <Text text="Hi!" />
2509
- <Suspense
2510
- fallback={<Text text={'Loading ' + page + '...'} />}
2511
- unstable_avoidThisFallback={true}>
2512
- <AsyncText text={page} ms={3000} />
2513
- </Suspense>
2514
- </Suspense>
2515
- );
2516
- }
2517
-
2518
- // Initial render.
2519
- ReactNoop.render(<App page="A" />);
2520
- expect(Scheduler).toFlushAndYield(['Hi!', 'Suspend! [A]', 'Loading...']);
2521
- Scheduler.unstable_advanceTime(3000);
2522
- await advanceTimers(3000);
2523
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2524
- expect(Scheduler).toFlushAndYield(['Hi!', 'A']);
2525
- expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2526
-
2527
- // Start transition.
2528
- React.unstable_withSuspenseConfig(
2529
- () => ReactNoop.render(<App page="B" />),
2530
- {timeoutMs: 2000},
2531
- );
2532
-
2533
- expect(Scheduler).toFlushAndYield(['Hi!', 'Suspend! [B]', 'Loading B...']);
2534
-
2535
- // Suspended
2536
- expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2537
- Scheduler.unstable_advanceTime(1800);
2538
- await advanceTimers(1800);
2539
- expect(Scheduler).toFlushAndYield([]);
2540
- // We should still be suspended here because this loading state should be avoided.
2541
- expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2542
- Scheduler.unstable_advanceTime(1500);
2543
- await advanceTimers(1500);
2544
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2545
- expect(ReactNoop.getChildren()).toEqual([
2546
- span('Hi!'),
2547
- hiddenSpan('A'),
2548
- span('Loading B...'),
This file is too large to show in full.
packages/shared/ReactFeatureFlags.js
+9
@@ -91,10 +91,19 @@ export const enableTrustedTypesIntegration = false;
91
// Flag to turn event.target and event.currentTarget in ReactNative from a reactTag to a component instance
92
export const enableNativeTargetAsInstance = false;
93
94
+// Controls sequence of passive effect destroy and create functions.
95
+// If this flag is off, destroy and create functions may be interleaved.
96
+// When the falg is on, all destroy functions will be run (for all fibers)
97
+// before any create functions are run, similar to how layout effects work.
98
+// This flag provides a killswitch if that proves to break existing code somehow.
99
+export const runAllPassiveEffectDestroysBeforeCreates = false;
100
+
101
// Controls behavior of deferred effect destroy functions during unmount.
102
// Previously these functions were run during commit (along with layout effects).
103
// Ideally we should delay these until after commit for performance reasons.
104
// This flag provides a killswitch if that proves to break existing code somehow.
105
+//
106
+// WARNING This flag only has an affect if used with runAllPassiveEffectDestroysBeforeCreates.
107
export const deferPassiveEffectCleanupDuringUnmount = false;
108
109
export const isTestEnvironment = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -53,6 +53,7 @@ export const disableUnstableRenderSubtreeIntoContainer = false;
53
export const warnUnstableRenderSubtreeIntoContainer = false;
54
export const disableUnstableCreatePortal = false;
55
export const deferPassiveEffectCleanupDuringUnmount = false;
56
+export const runAllPassiveEffectDestroysBeforeCreates = false;
57
export const isTestEnvironment = false;
58
59
// Only used in www builds.
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -48,6 +48,7 @@ export const disableUnstableRenderSubtreeIntoContainer = false;
48
export const warnUnstableRenderSubtreeIntoContainer = false;
49
export const disableUnstableCreatePortal = false;
50
export const deferPassiveEffectCleanupDuringUnmount = false;
51
+export const runAllPassiveEffectDestroysBeforeCreates = false;
52
export const isTestEnvironment = false;
53
54
// Only used in www builds.
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -48,6 +48,7 @@ export const disableUnstableRenderSubtreeIntoContainer = false;
48
export const warnUnstableRenderSubtreeIntoContainer = false;
49
export const disableUnstableCreatePortal = false;
50
export const deferPassiveEffectCleanupDuringUnmount = false;
51
+export const runAllPassiveEffectDestroysBeforeCreates = false;
52
export const isTestEnvironment = false;
53
54
// Only used in www builds.
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -48,6 +48,7 @@ export const disableUnstableRenderSubtreeIntoContainer = false;
48
export const warnUnstableRenderSubtreeIntoContainer = false;
49
export const disableUnstableCreatePortal = false;
50
export const deferPassiveEffectCleanupDuringUnmount = false;
51
+export const runAllPassiveEffectDestroysBeforeCreates = false;
52
export const isTestEnvironment = true; // this should probably *never* change
53
54
// Only used in www builds.
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -48,6 +48,7 @@ export const disableUnstableRenderSubtreeIntoContainer = false;
48
export const warnUnstableRenderSubtreeIntoContainer = false;
49
export const disableUnstableCreatePortal = false;
50
export const deferPassiveEffectCleanupDuringUnmount = false;
51
+export const runAllPassiveEffectDestroysBeforeCreates = false;
52
export const isTestEnvironment = true; // this should probably *never* change
53
54
// Only used in www builds.
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -48,6 +48,7 @@ export const disableUnstableRenderSubtreeIntoContainer = false;
48
export const warnUnstableRenderSubtreeIntoContainer = false;
49
export const disableUnstableCreatePortal = false;
50
export const deferPassiveEffectCleanupDuringUnmount = false;
51
+export const runAllPassiveEffectDestroysBeforeCreates = false;
52
export const isTestEnvironment = true;
53
54
// Only used in www builds.
packages/shared/forks/ReactFeatureFlags.www.js
+2
-1
@@ -13,9 +13,10 @@ import typeof * as ExportsType from './ReactFeatureFlags.www';
13
// Re-export dynamic flags from the www version.
14
export const {
15
debugRenderPhaseSideEffectsForStrictMode,
16
+ deferPassiveEffectCleanupDuringUnmount,
17
disableInputAttributeSyncing,
18
enableTrustedTypesIntegration,
18
- deferPassiveEffectCleanupDuringUnmount,
19
+ runAllPassiveEffectDestroysBeforeCreates,
20
warnAboutShorthandPropertyCollision,
21
disableSchedulerTimeoutBasedOnReactExpirationTime,
22
} = require('ReactFeatureFlags');