Remove manual feature flag overrides (#18291)
Tests now run against both versions of these flags, using the test-www-variant command. So we can remove the manual overrides.
Andrew Clark committed
Mar 12, 2020 at 10:17 UTC
615df434bbecc357d47ce26e3d6362edec477050
2 files changed
+5392
-5588
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+2711
-2850
@@ -33,3177 +33,3038 @@ let useDeferredValue;
33
let forwardRef;
34
let memo;
35
let act;
36
-
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
- ReactFeatureFlags.enableProfilerTimer = true;
48
- React = require('react');
49
- ReactNoop = require('react-noop-renderer');
50
- Scheduler = require('scheduler');
51
- SchedulerTracing = require('scheduler/tracing');
52
- ReactCache = require('react-cache');
53
- useState = React.useState;
54
- useReducer = React.useReducer;
55
- useEffect = React.useEffect;
56
- useLayoutEffect = React.useLayoutEffect;
57
- useCallback = React.useCallback;
58
- useMemo = React.useMemo;
59
- useRef = React.useRef;
60
- useImperativeHandle = React.useImperativeHandle;
61
- forwardRef = React.forwardRef;
62
- memo = React.memo;
63
- useTransition = React.useTransition;
64
- useDeferredValue = React.useDeferredValue;
65
- Suspense = React.Suspense;
66
- act = ReactNoop.act;
67
-}
68
-
69
-[true, false].forEach(deferPassiveEffectCleanupDuringUnmount => {
70
- [true, false].forEach(runAllPassiveEffectDestroysBeforeCreates => {
71
- describe(`ReactHooksWithNoopRenderer deferPassiveEffectCleanupDuringUnmount:${deferPassiveEffectCleanupDuringUnmount} runAllPassiveEffectDestroysBeforeCreates:${runAllPassiveEffectDestroysBeforeCreates}`, () => {
72
- beforeEach(() => {
73
- jest.resetModules();
74
- jest.useFakeTimers();
75
-
76
- loadModules({
77
- deferPassiveEffectCleanupDuringUnmount,
78
- runAllPassiveEffectDestroysBeforeCreates,
79
- });
80
-
81
- TextResource = ReactCache.unstable_createResource(
82
- ([text, ms = 0]) => {
83
- return new Promise((resolve, reject) =>
84
- setTimeout(() => {
85
- Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
86
- resolve(text);
87
- }, ms),
88
- );
89
- },
90
- ([text, ms]) => text,
36
+let deferPassiveEffectCleanupDuringUnmount;
37
+let runAllPassiveEffectDestroysBeforeCreates;
38
+
39
+describe('ReactHooksWithNoopRenderer', () => {
40
+ beforeEach(() => {
41
+ jest.resetModules();
42
+ jest.useFakeTimers();
43
+
44
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
45
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
46
+ ReactFeatureFlags.enableSchedulerTracing = true;
47
+ ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
48
+ ReactFeatureFlags.enableProfilerTimer = true;
49
+ deferPassiveEffectCleanupDuringUnmount =
50
+ ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount;
51
+ runAllPassiveEffectDestroysBeforeCreates =
52
+ ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates;
53
+
54
+ React = require('react');
55
+ ReactNoop = require('react-noop-renderer');
56
+ Scheduler = require('scheduler');
57
+ SchedulerTracing = require('scheduler/tracing');
58
+ ReactCache = require('react-cache');
59
+ useState = React.useState;
60
+ useReducer = React.useReducer;
61
+ useEffect = React.useEffect;
62
+ useLayoutEffect = React.useLayoutEffect;
63
+ useCallback = React.useCallback;
64
+ useMemo = React.useMemo;
65
+ useRef = React.useRef;
66
+ useImperativeHandle = React.useImperativeHandle;
67
+ forwardRef = React.forwardRef;
68
+ memo = React.memo;
69
+ useTransition = React.useTransition;
70
+ useDeferredValue = React.useDeferredValue;
71
+ Suspense = React.Suspense;
72
+ act = ReactNoop.act;
73
+
74
+ TextResource = ReactCache.unstable_createResource(
75
+ ([text, ms = 0]) => {
76
+ return new Promise((resolve, reject) =>
77
+ setTimeout(() => {
78
+ Scheduler.unstable_yieldValue(`Promise resolved [${text}]`);
79
+ resolve(text);
80
+ }, ms),
81
);
92
- });
82
+ },
83
+ ([text, ms]) => text,
84
+ );
85
+ });
86
94
- function span(prop) {
95
- return {type: 'span', hidden: false, children: [], prop};
87
+ function span(prop) {
88
+ return {type: 'span', hidden: false, children: [], prop};
89
+ }
90
+
91
+ function hiddenSpan(prop) {
92
+ return {type: 'span', children: [], prop, hidden: true};
93
+ }
94
+
95
+ function Text(props) {
96
+ Scheduler.unstable_yieldValue(props.text);
97
+ return <span prop={props.text} />;
98
+ }
99
+
100
+ function AsyncText(props) {
101
+ const text = props.text;
102
+ try {
103
+ TextResource.read([props.text, props.ms]);
104
+ Scheduler.unstable_yieldValue(text);
105
+ return <span prop={text} />;
106
+ } catch (promise) {
107
+ if (typeof promise.then === 'function') {
108
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
109
+ } else {
110
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
111
}
112
+ throw promise;
113
+ }
114
+ }
115
+
116
+ function advanceTimers(ms) {
117
+ // Note: This advances Jest's virtual time but not React's. Use
118
+ // ReactNoop.expire for that.
119
+ if (typeof ms !== 'number') {
120
+ throw new Error('Must specify ms');
121
+ }
122
+ jest.advanceTimersByTime(ms);
123
+ // Wait until the end of the current tick
124
+ // We cannot use a timer since we're faking them
125
+ return Promise.resolve().then(() => {});
126
+ }
127
+
128
+ it('resumes after an interruption', () => {
129
+ function Counter(props, ref) {
130
+ const [count, updateCount] = useState(0);
131
+ useImperativeHandle(ref, () => ({updateCount}));
132
+ return <Text text={props.label + ': ' + count} />;
133
+ }
134
+ Counter = forwardRef(Counter);
135
+
136
+ // Initial mount
137
+ const counter = React.createRef(null);
138
+ ReactNoop.render(<Counter label="Count" ref={counter} />);
139
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
140
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
141
+
142
+ // Schedule some updates
143
+ ReactNoop.batchedUpdates(() => {
144
+ counter.current.updateCount(1);
145
+ counter.current.updateCount(count => count + 10);
146
+ });
147
98
- function hiddenSpan(prop) {
99
- return {type: 'span', children: [], prop, hidden: true};
100
- }
148
+ // Partially flush without committing
149
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 11']);
150
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
151
102
- function Text(props) {
103
- Scheduler.unstable_yieldValue(props.text);
104
- return <span prop={props.text} />;
105
- }
152
+ // Interrupt with a high priority update
153
+ ReactNoop.flushSync(() => {
154
+ ReactNoop.render(<Counter label="Total" />);
155
+ });
156
+ expect(Scheduler).toHaveYielded(['Total: 0']);
157
107
- function AsyncText(props) {
108
- const text = props.text;
109
- try {
110
- TextResource.read([props.text, props.ms]);
111
- Scheduler.unstable_yieldValue(text);
112
- return <span prop={text} />;
113
- } catch (promise) {
114
- if (typeof promise.then === 'function') {
115
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
116
- } else {
117
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
118
- }
119
- throw promise;
120
- }
121
- }
158
+ // Resume rendering
159
+ expect(Scheduler).toFlushAndYield(['Total: 11']);
160
+ expect(ReactNoop.getChildren()).toEqual([span('Total: 11')]);
161
+ });
162
123
- function advanceTimers(ms) {
124
- // Note: This advances Jest's virtual time but not React's. Use
125
- // ReactNoop.expire for that.
126
- if (typeof ms !== 'number') {
127
- throw new Error('Must specify ms');
128
- }
129
- jest.advanceTimersByTime(ms);
130
- // Wait until the end of the current tick
131
- // We cannot use a timer since we're faking them
132
- return Promise.resolve().then(() => {});
163
+ it('throws inside class components', () => {
164
+ class BadCounter extends React.Component {
165
+ render() {
166
+ const [count] = useState(0);
167
+ return <Text text={this.props.label + ': ' + count} />;
168
}
169
+ }
170
+ ReactNoop.render(<BadCounter />);
171
+
172
+ expect(Scheduler).toFlushAndThrow(
173
+ 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
174
+ ' one of the following reasons:\n' +
175
+ '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
176
+ '2. You might be breaking the Rules of Hooks\n' +
177
+ '3. You might have more than one copy of React in the same app\n' +
178
+ 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
179
+ );
180
+
181
+ // Confirm that a subsequent hook works properly.
182
+ function GoodCounter(props, ref) {
183
+ const [count] = useState(props.initialCount);
184
+ return <Text text={count} />;
185
+ }
186
+ ReactNoop.render(<GoodCounter initialCount={10} />);
187
+ expect(Scheduler).toFlushAndYield([10]);
188
+ });
189
135
- it('resumes after an interruption', () => {
136
- function Counter(props, ref) {
137
- const [count, updateCount] = useState(0);
138
- useImperativeHandle(ref, () => ({updateCount}));
139
- return <Text text={props.label + ': ' + count} />;
140
- }
141
- Counter = forwardRef(Counter);
142
-
143
- // Initial mount
144
- const counter = React.createRef(null);
145
- ReactNoop.render(<Counter label="Count" ref={counter} />);
146
- expect(Scheduler).toFlushAndYield(['Count: 0']);
147
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
190
+ it('throws inside module-style components', () => {
191
+ function Counter() {
192
+ return {
193
+ render() {
194
+ const [count] = useState(0);
195
+ return <Text text={this.props.label + ': ' + count} />;
196
+ },
197
+ };
198
+ }
199
+ ReactNoop.render(<Counter />);
200
+ expect(() =>
201
+ expect(Scheduler).toFlushAndThrow(
202
+ 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen ' +
203
+ 'for one of the following reasons:\n' +
204
+ '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
205
+ '2. You might be breaking the Rules of Hooks\n' +
206
+ '3. You might have more than one copy of React in the same app\n' +
207
+ 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
208
+ ),
209
+ ).toErrorDev(
210
+ 'Warning: The <Counter /> component appears to be a function component that returns a class instance. ' +
211
+ 'Change Counter to a class that extends React.Component instead. ' +
212
+ "If you can't use a class try assigning the prototype on the function as a workaround. " +
213
+ '`Counter.prototype = React.Component.prototype`. ' +
214
+ "Don't use an arrow function since it cannot be called with `new` by React.",
215
+ );
216
+
217
+ // Confirm that a subsequent hook works properly.
218
+ function GoodCounter(props) {
219
+ const [count] = useState(props.initialCount);
220
+ return <Text text={count} />;
221
+ }
222
+ ReactNoop.render(<GoodCounter initialCount={10} />);
223
+ expect(Scheduler).toFlushAndYield([10]);
224
+ });
225
149
- // Schedule some updates
150
- ReactNoop.batchedUpdates(() => {
151
- counter.current.updateCount(1);
152
- counter.current.updateCount(count => count + 10);
153
- });
226
+ it('throws when called outside the render phase', () => {
227
+ expect(() => useState(0)).toThrow(
228
+ 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
229
+ ' one of the following reasons:\n' +
230
+ '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
231
+ '2. You might be breaking the Rules of Hooks\n' +
232
+ '3. You might have more than one copy of React in the same app\n' +
233
+ 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
234
+ );
235
+ });
236
155
- // Partially flush without committing
156
- expect(Scheduler).toFlushAndYieldThrough(['Count: 11']);
157
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
237
+ describe('useState', () => {
238
+ it('simple mount and update', () => {
239
+ function Counter(props, ref) {
240
+ const [count, updateCount] = useState(0);
241
+ useImperativeHandle(ref, () => ({updateCount}));
242
+ return <Text text={'Count: ' + count} />;
243
+ }
244
+ Counter = forwardRef(Counter);
245
+ const counter = React.createRef(null);
246
+ ReactNoop.render(<Counter ref={counter} />);
247
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
248
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
249
+
250
+ act(() => counter.current.updateCount(1));
251
+ expect(Scheduler).toHaveYielded(['Count: 1']);
252
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
253
+
254
+ act(() => counter.current.updateCount(count => count + 10));
255
+ expect(Scheduler).toHaveYielded(['Count: 11']);
256
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
257
+ });
258
159
- // Interrupt with a high priority update
160
- ReactNoop.flushSync(() => {
161
- ReactNoop.render(<Counter label="Total" />);
259
+ it('lazy state initializer', () => {
260
+ function Counter(props, ref) {
261
+ const [count, updateCount] = useState(() => {
262
+ Scheduler.unstable_yieldValue('getInitialState');
263
+ return props.initialState;
264
});
163
- expect(Scheduler).toHaveYielded(['Total: 0']);
265
+ useImperativeHandle(ref, () => ({updateCount}));
266
+ return <Text text={'Count: ' + count} />;
267
+ }
268
+ Counter = forwardRef(Counter);
269
+ const counter = React.createRef(null);
270
+ ReactNoop.render(<Counter initialState={42} ref={counter} />);
271
+ expect(Scheduler).toFlushAndYield(['getInitialState', 'Count: 42']);
272
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]);
273
+
274
+ act(() => counter.current.updateCount(7));
275
+ expect(Scheduler).toHaveYielded(['Count: 7']);
276
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]);
277
+ });
278
165
- // Resume rendering
166
- expect(Scheduler).toFlushAndYield(['Total: 11']);
167
- expect(ReactNoop.getChildren()).toEqual([span('Total: 11')]);
168
- });
279
+ it('multiple states', () => {
280
+ function Counter(props, ref) {
281
+ const [count, updateCount] = useState(0);
282
+ const [label, updateLabel] = useState('Count');
283
+ useImperativeHandle(ref, () => ({updateCount, updateLabel}));
284
+ return <Text text={label + ': ' + count} />;
285
+ }
286
+ Counter = forwardRef(Counter);
287
+ const counter = React.createRef(null);
288
+ ReactNoop.render(<Counter ref={counter} />);
289
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
290
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
291
170
- it('throws inside class components', () => {
171
- class BadCounter extends React.Component {
172
- render() {
173
- const [count] = useState(0);
174
- return <Text text={this.props.label + ': ' + count} />;
175
- }
176
- }
177
- ReactNoop.render(<BadCounter />);
178
-
179
- expect(Scheduler).toFlushAndThrow(
180
- 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
181
- ' one of the following reasons:\n' +
182
- '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
183
- '2. You might be breaking the Rules of Hooks\n' +
184
- '3. You might have more than one copy of React in the same app\n' +
185
- 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
186
- );
292
+ act(() => counter.current.updateCount(7));
293
+ expect(Scheduler).toHaveYielded(['Count: 7']);
294
188
- // Confirm that a subsequent hook works properly.
189
- function GoodCounter(props, ref) {
190
- const [count] = useState(props.initialCount);
191
- return <Text text={count} />;
192
- }
193
- ReactNoop.render(<GoodCounter initialCount={10} />);
194
- expect(Scheduler).toFlushAndYield([10]);
195
- });
295
+ act(() => counter.current.updateLabel('Total'));
296
+ expect(Scheduler).toHaveYielded(['Total: 7']);
297
+ });
298
197
- it('throws inside module-style components', () => {
198
- function Counter() {
199
- return {
200
- render() {
201
- const [count] = useState(0);
202
- return <Text text={this.props.label + ': ' + count} />;
203
- },
204
- };
205
- }
206
- ReactNoop.render(<Counter />);
207
- expect(() =>
208
- expect(Scheduler).toFlushAndThrow(
209
- 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen ' +
210
- 'for one of the following reasons:\n' +
211
- '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
212
- '2. You might be breaking the Rules of Hooks\n' +
213
- '3. You might have more than one copy of React in the same app\n' +
214
- 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
215
- ),
216
- ).toErrorDev(
217
- 'Warning: The <Counter /> component appears to be a function component that returns a class instance. ' +
218
- 'Change Counter to a class that extends React.Component instead. ' +
219
- "If you can't use a class try assigning the prototype on the function as a workaround. " +
220
- '`Counter.prototype = React.Component.prototype`. ' +
221
- "Don't use an arrow function since it cannot be called with `new` by React.",
222
- );
299
+ it('returns the same updater function every time', () => {
300
+ let updaters = [];
301
+ function Counter() {
302
+ const [count, updateCount] = useState(0);
303
+ updaters.push(updateCount);
304
+ return <Text text={'Count: ' + count} />;
305
+ }
306
+ ReactNoop.render(<Counter />);
307
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
308
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
309
224
- // Confirm that a subsequent hook works properly.
225
- function GoodCounter(props) {
226
- const [count] = useState(props.initialCount);
227
- return <Text text={count} />;
228
- }
229
- ReactNoop.render(<GoodCounter initialCount={10} />);
230
- expect(Scheduler).toFlushAndYield([10]);
231
- });
310
+ act(() => updaters[0](1));
311
+ expect(Scheduler).toHaveYielded(['Count: 1']);
312
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
313
233
- it('throws when called outside the render phase', () => {
234
- expect(() => useState(0)).toThrow(
235
- 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
236
- ' one of the following reasons:\n' +
237
- '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
238
- '2. You might be breaking the Rules of Hooks\n' +
239
- '3. You might have more than one copy of React in the same app\n' +
240
- 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
241
- );
242
- });
314
+ act(() => updaters[0](count => count + 10));
315
+ expect(Scheduler).toHaveYielded(['Count: 11']);
316
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
317
244
- describe('useState', () => {
245
- it('simple mount and update', () => {
246
- function Counter(props, ref) {
247
- const [count, updateCount] = useState(0);
248
- useImperativeHandle(ref, () => ({updateCount}));
249
- return <Text text={'Count: ' + count} />;
250
- }
251
- Counter = forwardRef(Counter);
252
- const counter = React.createRef(null);
253
- ReactNoop.render(<Counter ref={counter} />);
254
- expect(Scheduler).toFlushAndYield(['Count: 0']);
255
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
256
-
257
- act(() => counter.current.updateCount(1));
258
- expect(Scheduler).toHaveYielded(['Count: 1']);
259
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
318
+ expect(updaters).toEqual([updaters[0], updaters[0], updaters[0]]);
319
+ });
320
261
- act(() => counter.current.updateCount(count => count + 10));
262
- expect(Scheduler).toHaveYielded(['Count: 11']);
263
- expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
264
- });
321
+ it('warns on set after unmount', () => {
322
+ let _updateCount;
323
+ function Counter(props, ref) {
324
+ const [, updateCount] = useState(0);
325
+ _updateCount = updateCount;
326
+ return null;
327
+ }
328
266
- it('lazy state initializer', () => {
267
- function Counter(props, ref) {
268
- const [count, updateCount] = useState(() => {
269
- Scheduler.unstable_yieldValue('getInitialState');
270
- return props.initialState;
271
- });
272
- useImperativeHandle(ref, () => ({updateCount}));
273
- return <Text text={'Count: ' + count} />;
274
- }
275
- Counter = forwardRef(Counter);
276
- const counter = React.createRef(null);
277
- ReactNoop.render(<Counter initialState={42} ref={counter} />);
278
- expect(Scheduler).toFlushAndYield(['getInitialState', 'Count: 42']);
279
- expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]);
280
-
281
- act(() => counter.current.updateCount(7));
282
- expect(Scheduler).toHaveYielded(['Count: 7']);
283
- expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]);
284
- });
329
+ ReactNoop.render(<Counter />);
330
+ expect(Scheduler).toFlushWithoutYielding();
331
+ ReactNoop.render(null);
332
+ expect(Scheduler).toFlushWithoutYielding();
333
+ expect(() => act(() => _updateCount(1))).toErrorDev(
334
+ "Warning: Can't perform a React state update on an unmounted " +
335
+ 'component. This is a no-op, but it indicates a memory leak in your ' +
336
+ 'application. To fix, cancel all subscriptions and asynchronous ' +
337
+ 'tasks in a useEffect cleanup function.\n' +
338
+ ' in Counter (at **)',
339
+ );
340
+ });
341
286
- it('multiple states', () => {
287
- function Counter(props, ref) {
288
- const [count, updateCount] = useState(0);
289
- const [label, updateLabel] = useState('Count');
290
- useImperativeHandle(ref, () => ({updateCount, updateLabel}));
291
- return <Text text={label + ': ' + count} />;
292
- }
293
- Counter = forwardRef(Counter);
294
- const counter = React.createRef(null);
295
- ReactNoop.render(<Counter ref={counter} />);
296
- expect(Scheduler).toFlushAndYield(['Count: 0']);
297
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
342
+ it('works with memo', () => {
343
+ let _updateCount;
344
+ function Counter(props) {
345
+ const [count, updateCount] = useState(0);
346
+ _updateCount = updateCount;
347
+ return <Text text={'Count: ' + count} />;
348
+ }
349
+ Counter = memo(Counter);
350
299
- act(() => counter.current.updateCount(7));
300
- expect(Scheduler).toHaveYielded(['Count: 7']);
351
+ ReactNoop.render(<Counter />);
352
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
353
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
354
302
- act(() => counter.current.updateLabel('Total'));
303
- expect(Scheduler).toHaveYielded(['Total: 7']);
304
- });
355
+ ReactNoop.render(<Counter />);
356
+ expect(Scheduler).toFlushAndYield([]);
357
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
358
306
- it('returns the same updater function every time', () => {
307
- let updaters = [];
308
- function Counter() {
309
- const [count, updateCount] = useState(0);
310
- updaters.push(updateCount);
311
- return <Text text={'Count: ' + count} />;
312
- }
313
- ReactNoop.render(<Counter />);
314
- expect(Scheduler).toFlushAndYield(['Count: 0']);
315
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
359
+ act(() => _updateCount(1));
360
+ expect(Scheduler).toHaveYielded(['Count: 1']);
361
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
362
+ });
363
+ });
364
317
- act(() => updaters[0](1));
318
- expect(Scheduler).toHaveYielded(['Count: 1']);
319
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
365
+ describe('updates during the render phase', () => {
366
+ it('restarts the render function and applies the new updates on top', () => {
367
+ function ScrollView({row: newRow}) {
368
+ let [isScrollingDown, setIsScrollingDown] = useState(false);
369
+ let [row, setRow] = useState(null);
370
321
- act(() => updaters[0](count => count + 10));
322
- expect(Scheduler).toHaveYielded(['Count: 11']);
323
- expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
371
+ if (row !== newRow) {
372
+ // Row changed since last render. Update isScrollingDown.
373
+ setIsScrollingDown(row !== null && newRow > row);
374
+ setRow(newRow);
375
+ }
376
325
- expect(updaters).toEqual([updaters[0], updaters[0], updaters[0]]);
326
- });
377
+ return <Text text={`Scrolling down: ${isScrollingDown}`} />;
378
+ }
379
328
- it('warns on set after unmount', () => {
329
- let _updateCount;
330
- function Counter(props, ref) {
331
- const [, updateCount] = useState(0);
332
- _updateCount = updateCount;
333
- return null;
334
- }
380
+ ReactNoop.render(<ScrollView row={1} />);
381
+ expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
382
+ expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: false')]);
383
336
- ReactNoop.render(<Counter />);
337
- expect(Scheduler).toFlushWithoutYielding();
338
- ReactNoop.render(null);
339
- expect(Scheduler).toFlushWithoutYielding();
340
- expect(() => act(() => _updateCount(1))).toErrorDev(
341
- "Warning: Can't perform a React state update on an unmounted " +
342
- 'component. This is a no-op, but it indicates a memory leak in your ' +
343
- 'application. To fix, cancel all subscriptions and asynchronous ' +
344
- 'tasks in a useEffect cleanup function.\n' +
345
- ' in Counter (at **)',
346
- );
347
- });
384
+ ReactNoop.render(<ScrollView row={5} />);
385
+ expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
386
+ expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: true')]);
387
349
- it('works with memo', () => {
350
- let _updateCount;
351
- function Counter(props) {
352
- const [count, updateCount] = useState(0);
353
- _updateCount = updateCount;
354
- return <Text text={'Count: ' + count} />;
355
- }
356
- Counter = memo(Counter);
388
+ ReactNoop.render(<ScrollView row={5} />);
389
+ expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
390
+ expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: true')]);
391
358
- ReactNoop.render(<Counter />);
359
- expect(Scheduler).toFlushAndYield(['Count: 0']);
360
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
392
+ ReactNoop.render(<ScrollView row={10} />);
393
+ expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
394
+ expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: true')]);
395
362
- ReactNoop.render(<Counter />);
363
- expect(Scheduler).toFlushAndYield([]);
364
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
396
+ ReactNoop.render(<ScrollView row={2} />);
397
+ expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
398
+ expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: false')]);
399
366
- act(() => _updateCount(1));
367
- expect(Scheduler).toHaveYielded(['Count: 1']);
368
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
369
- });
370
- });
400
+ ReactNoop.render(<ScrollView row={2} />);
401
+ expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
402
+ expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: false')]);
403
+ });
404
372
- describe('updates during the render phase', () => {
373
- it('restarts the render function and applies the new updates on top', () => {
374
- function ScrollView({row: newRow}) {
375
- let [isScrollingDown, setIsScrollingDown] = useState(false);
376
- let [row, setRow] = useState(null);
405
+ it('warns about render phase update on a different component', async () => {
406
+ let setStep;
407
+ function Foo() {
408
+ const [step, _setStep] = useState(0);
409
+ setStep = _setStep;
410
+ return <Text text={`Foo [${step}]`} />;
411
+ }
412
378
- if (row !== newRow) {
379
- // Row changed since last render. Update isScrollingDown.
380
- setIsScrollingDown(row !== null && newRow > row);
381
- setRow(newRow);
382
- }
413
+ function Bar({triggerUpdate}) {
414
+ if (triggerUpdate) {
415
+ setStep(1);
416
+ }
417
+ return <Text text="Bar" />;
418
+ }
419
384
- return <Text text={`Scrolling down: ${isScrollingDown}`} />;
385
- }
420
+ const root = ReactNoop.createRoot();
421
387
- ReactNoop.render(<ScrollView row={1} />);
388
- expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
389
- expect(ReactNoop.getChildren()).toEqual([
390
- span('Scrolling down: false'),
391
- ]);
422
+ await ReactNoop.act(async () => {
423
+ root.render(
424
+ <>
425
+ <Foo />
426
+ <Bar />
427
+ </>,
428
+ );
429
+ });
430
+ expect(Scheduler).toHaveYielded(['Foo [0]', 'Bar']);
431
+
432
+ // Bar will update Foo during its render phase. React should warn.
433
+ await ReactNoop.act(async () => {
434
+ root.render(
435
+ <>
436
+ <Foo />
437
+ <Bar triggerUpdate={true} />
438
+ </>,
439
+ );
440
+ expect(() =>
441
+ expect(Scheduler).toFlushAndYield(['Foo [0]', 'Bar', 'Foo [1]']),
442
+ ).toErrorDev([
443
+ 'Cannot update a component from inside the function body of a ' +
444
+ 'different component.',
445
+ ]);
446
+ });
447
+ });
448
393
- ReactNoop.render(<ScrollView row={5} />);
394
- expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
395
- expect(ReactNoop.getChildren()).toEqual([
396
- span('Scrolling down: true'),
397
- ]);
449
+ it('keeps restarting until there are no more new updates', () => {
450
+ function Counter({row: newRow}) {
451
+ let [count, setCount] = useState(0);
452
+ if (count < 3) {
453
+ setCount(count + 1);
454
+ }
455
+ Scheduler.unstable_yieldValue('Render: ' + count);
456
+ return <Text text={count} />;
457
+ }
458
399
- ReactNoop.render(<ScrollView row={5} />);
400
- expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
401
- expect(ReactNoop.getChildren()).toEqual([
402
- span('Scrolling down: true'),
403
- ]);
459
+ ReactNoop.render(<Counter />);
460
+ expect(Scheduler).toFlushAndYield([
461
+ 'Render: 0',
462
+ 'Render: 1',
463
+ 'Render: 2',
464
+ 'Render: 3',
465
+ 3,
466
+ ]);
467
+ expect(ReactNoop.getChildren()).toEqual([span(3)]);
468
+ });
469
405
- ReactNoop.render(<ScrollView row={10} />);
406
- expect(Scheduler).toFlushAndYield(['Scrolling down: true']);
407
- expect(ReactNoop.getChildren()).toEqual([
408
- span('Scrolling down: true'),
409
- ]);
470
+ it('updates multiple times within same render function', () => {
471
+ function Counter({row: newRow}) {
472
+ let [count, setCount] = useState(0);
473
+ if (count < 12) {
474
+ setCount(c => c + 1);
475
+ setCount(c => c + 1);
476
+ setCount(c => c + 1);
477
+ }
478
+ Scheduler.unstable_yieldValue('Render: ' + count);
479
+ return <Text text={count} />;
480
+ }
481
411
- ReactNoop.render(<ScrollView row={2} />);
412
- expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
413
- expect(ReactNoop.getChildren()).toEqual([
414
- span('Scrolling down: false'),
415
- ]);
482
+ ReactNoop.render(<Counter />);
483
+ expect(Scheduler).toFlushAndYield([
484
+ // Should increase by three each time
485
+ 'Render: 0',
486
+ 'Render: 3',
487
+ 'Render: 6',
488
+ 'Render: 9',
489
+ 'Render: 12',
490
+ 12,
491
+ ]);
492
+ expect(ReactNoop.getChildren()).toEqual([span(12)]);
493
+ });
494
417
- ReactNoop.render(<ScrollView row={2} />);
418
- expect(Scheduler).toFlushAndYield(['Scrolling down: false']);
419
- expect(ReactNoop.getChildren()).toEqual([
420
- span('Scrolling down: false'),
421
- ]);
422
- });
495
+ it('throws after too many iterations', () => {
496
+ function Counter({row: newRow}) {
497
+ let [count, setCount] = useState(0);
498
+ setCount(count + 1);
499
+ Scheduler.unstable_yieldValue('Render: ' + count);
500
+ return <Text text={count} />;
501
+ }
502
+ ReactNoop.render(<Counter />);
503
+ expect(Scheduler).toFlushAndThrow(
504
+ 'Too many re-renders. React limits the number of renders to prevent ' +
505
+ 'an infinite loop.',
506
+ );
507
+ });
508
424
- it('warns about render phase update on a different component', async () => {
425
- let setStep;
426
- function Foo() {
427
- const [step, _setStep] = useState(0);
428
- setStep = _setStep;
429
- return <Text text={`Foo [${step}]`} />;
430
- }
509
+ it('works with useReducer', () => {
510
+ function reducer(state, action) {
511
+ return action === 'increment' ? state + 1 : state;
512
+ }
513
+ function Counter({row: newRow}) {
514
+ let [count, dispatch] = useReducer(reducer, 0);
515
+ if (count < 3) {
516
+ dispatch('increment');
517
+ }
518
+ Scheduler.unstable_yieldValue('Render: ' + count);
519
+ return <Text text={count} />;
520
+ }
521
432
- function Bar({triggerUpdate}) {
433
- if (triggerUpdate) {
434
- setStep(1);
435
- }
436
- return <Text text="Bar" />;
437
- }
522
+ ReactNoop.render(<Counter />);
523
+ expect(Scheduler).toFlushAndYield([
524
+ 'Render: 0',
525
+ 'Render: 1',
526
+ 'Render: 2',
527
+ 'Render: 3',
528
+ 3,
529
+ ]);
530
+ expect(ReactNoop.getChildren()).toEqual([span(3)]);
531
+ });
532
439
- const root = ReactNoop.createRoot();
533
+ it('uses reducer passed at time of render, not time of dispatch', () => {
534
+ // This test is a bit contrived but it demonstrates a subtle edge case.
535
441
- await ReactNoop.act(async () => {
442
- root.render(
443
- <>
444
- <Foo />
445
- <Bar />
446
- </>,
447
- );
448
- });
449
- expect(Scheduler).toHaveYielded(['Foo [0]', 'Bar']);
450
-
451
- // Bar will update Foo during its render phase. React should warn.
452
- await ReactNoop.act(async () => {
453
- root.render(
454
- <>
455
- <Foo />
456
- <Bar triggerUpdate={true} />
457
- </>,
458
- );
459
- expect(() =>
460
- expect(Scheduler).toFlushAndYield(['Foo [0]', 'Bar', 'Foo [1]']),
461
- ).toErrorDev([
462
- 'Cannot update a component from inside the function body of a ' +
463
- 'different component.',
464
- ]);
465
- });
466
- });
536
+ // Reducer A increments by 1. Reducer B increments by 10.
537
+ function reducerA(state, action) {
538
+ switch (action) {
539
+ case 'increment':
540
+ return state + 1;
541
+ case 'reset':
542
+ return 0;
543
+ }
544
+ }
545
+ function reducerB(state, action) {
546
+ switch (action) {
547
+ case 'increment':
548
+ return state + 10;
549
+ case 'reset':
550
+ return 0;
551
+ }
552
+ }
553
468
- it('keeps restarting until there are no more new updates', () => {
469
- function Counter({row: newRow}) {
470
- let [count, setCount] = useState(0);
471
- if (count < 3) {
472
- setCount(count + 1);
473
- }
474
- Scheduler.unstable_yieldValue('Render: ' + count);
475
- return <Text text={count} />;
554
+ function Counter({row: newRow}, ref) {
555
+ let [reducer, setReducer] = useState(() => reducerA);
556
+ let [count, dispatch] = useReducer(reducer, 0);
557
+ useImperativeHandle(ref, () => ({dispatch}));
558
+ if (count < 20) {
559
+ dispatch('increment');
560
+ // Swap reducers each time we increment
561
+ if (reducer === reducerA) {
562
+ setReducer(() => reducerB);
563
+ } else {
564
+ setReducer(() => reducerA);
565
}
566
+ }
567
+ Scheduler.unstable_yieldValue('Render: ' + count);
568
+ return <Text text={count} />;
569
+ }
570
+ Counter = forwardRef(Counter);
571
+ const counter = React.createRef(null);
572
+ ReactNoop.render(<Counter ref={counter} />);
573
+ expect(Scheduler).toFlushAndYield([
574
+ // The count should increase by alternating amounts of 10 and 1
575
+ // until we reach 21.
576
+ 'Render: 0',
577
+ 'Render: 10',
578
+ 'Render: 11',
579
+ 'Render: 21',
580
+ 21,
581
+ ]);
582
+ expect(ReactNoop.getChildren()).toEqual([span(21)]);
583
+
584
+ // Test that it works on update, too. This time the log is a bit different
585
+ // because we started with reducerB instead of reducerA.
586
+ ReactNoop.act(() => {
587
+ counter.current.dispatch('reset');
588
+ });
589
+ ReactNoop.render(<Counter ref={counter} />);
590
+ expect(Scheduler).toHaveYielded([
591
+ 'Render: 0',
592
+ 'Render: 1',
593
+ 'Render: 11',
594
+ 'Render: 12',
595
+ 'Render: 22',
596
+ 22,
597
+ ]);
598
+ expect(ReactNoop.getChildren()).toEqual([span(22)]);
599
+ });
600
478
- ReactNoop.render(<Counter />);
479
- expect(Scheduler).toFlushAndYield([
480
- 'Render: 0',
481
- 'Render: 1',
482
- 'Render: 2',
483
- 'Render: 3',
484
- 3,
485
- ]);
486
- expect(ReactNoop.getChildren()).toEqual([span(3)]);
487
- });
488
-
489
- it('updates multiple times within same render function', () => {
490
- function Counter({row: newRow}) {
491
- let [count, setCount] = useState(0);
492
- if (count < 12) {
493
- setCount(c => c + 1);
494
- setCount(c => c + 1);
495
- setCount(c => c + 1);
496
- }
497
- Scheduler.unstable_yieldValue('Render: ' + count);
498
- return <Text text={count} />;
499
- }
601
+ it('discards render phase updates if something suspends', () => {
602
+ const thenable = {then() {}};
603
+ function Foo({signal}) {
604
+ return (
605
+ <Suspense fallback="Loading...">
606
+ <Bar signal={signal} />
607
+ </Suspense>
608
+ );
609
+ }
610
501
- ReactNoop.render(<Counter />);
502
- expect(Scheduler).toFlushAndYield([
503
- // Should increase by three each time
504
- 'Render: 0',
505
- 'Render: 3',
506
- 'Render: 6',
507
- 'Render: 9',
508
- 'Render: 12',
509
- 12,
510
- ]);
511
- expect(ReactNoop.getChildren()).toEqual([span(12)]);
512
- });
611
+ function Bar({signal: newSignal}) {
612
+ let [counter, setCounter] = useState(0);
613
+ let [signal, setSignal] = useState(true);
614
514
- it('throws after too many iterations', () => {
515
- function Counter({row: newRow}) {
516
- let [count, setCount] = useState(0);
517
- setCount(count + 1);
518
- Scheduler.unstable_yieldValue('Render: ' + count);
519
- return <Text text={count} />;
615
+ // Increment a counter every time the signal changes
616
+ if (signal !== newSignal) {
617
+ setCounter(c => c + 1);
618
+ setSignal(newSignal);
619
+ if (counter === 0) {
620
+ // We're suspending during a render that includes render phase
621
+ // updates. Those updates should not persist to the next render.
622
+ Scheduler.unstable_yieldValue('Suspend!');
623
+ throw thenable;
624
}
521
- ReactNoop.render(<Counter />);
522
- expect(Scheduler).toFlushAndThrow(
523
- 'Too many re-renders. React limits the number of renders to prevent ' +
524
- 'an infinite loop.',
525
- );
526
- });
625
+ }
626
528
- it('works with useReducer', () => {
529
- function reducer(state, action) {
530
- return action === 'increment' ? state + 1 : state;
531
- }
532
- function Counter({row: newRow}) {
533
- let [count, dispatch] = useReducer(reducer, 0);
534
- if (count < 3) {
535
- dispatch('increment');
536
- }
537
- Scheduler.unstable_yieldValue('Render: ' + count);
538
- return <Text text={count} />;
539
- }
627
+ return <Text text={counter} />;
628
+ }
629
541
- ReactNoop.render(<Counter />);
542
- expect(Scheduler).toFlushAndYield([
543
- 'Render: 0',
544
- 'Render: 1',
545
- 'Render: 2',
546
- 'Render: 3',
547
- 3,
548
- ]);
549
- expect(ReactNoop.getChildren()).toEqual([span(3)]);
550
- });
630
+ const root = ReactNoop.createRoot();
631
+ root.render(<Foo signal={true} />);
632
552
- it('uses reducer passed at time of render, not time of dispatch', () => {
553
- // This test is a bit contrived but it demonstrates a subtle edge case.
633
+ expect(Scheduler).toFlushAndYield([0]);
634
+ expect(root).toMatchRenderedOutput(<span prop={0} />);
635
555
- // Reducer A increments by 1. Reducer B increments by 10.
556
- function reducerA(state, action) {
557
- switch (action) {
558
- case 'increment':
559
- return state + 1;
560
- case 'reset':
561
- return 0;
562
- }
563
- }
564
- function reducerB(state, action) {
565
- switch (action) {
566
- case 'increment':
567
- return state + 10;
568
- case 'reset':
569
- return 0;
570
- }
571
- }
636
+ root.render(<Foo signal={false} />);
637
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
638
+ expect(root).toMatchRenderedOutput(<span prop={0} />);
639
573
- function Counter({row: newRow}, ref) {
574
- let [reducer, setReducer] = useState(() => reducerA);
575
- let [count, dispatch] = useReducer(reducer, 0);
576
- useImperativeHandle(ref, () => ({dispatch}));
577
- if (count < 20) {
578
- dispatch('increment');
579
- // Swap reducers each time we increment
580
- if (reducer === reducerA) {
581
- setReducer(() => reducerB);
582
- } else {
583
- setReducer(() => reducerA);
584
- }
585
- }
586
- Scheduler.unstable_yieldValue('Render: ' + count);
587
- return <Text text={count} />;
588
- }
589
- Counter = forwardRef(Counter);
590
- const counter = React.createRef(null);
591
- ReactNoop.render(<Counter ref={counter} />);
592
- expect(Scheduler).toFlushAndYield([
593
- // The count should increase by alternating amounts of 10 and 1
594
- // until we reach 21.
595
- 'Render: 0',
596
- 'Render: 10',
597
- 'Render: 11',
598
- 'Render: 21',
599
- 21,
600
- ]);
601
- expect(ReactNoop.getChildren()).toEqual([span(21)]);
640
+ // Rendering again should suspend again.
641
+ root.render(<Foo signal={false} />);
642
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
643
+ });
644
603
- // Test that it works on update, too. This time the log is a bit different
604
- // because we started with reducerB instead of reducerA.
605
- ReactNoop.act(() => {
606
- counter.current.dispatch('reset');
607
- });
608
- ReactNoop.render(<Counter ref={counter} />);
609
- expect(Scheduler).toHaveYielded([
610
- 'Render: 0',
611
- 'Render: 1',
612
- 'Render: 11',
613
- 'Render: 12',
614
- 'Render: 22',
615
- 22,
616
- ]);
617
- expect(ReactNoop.getChildren()).toEqual([span(22)]);
618
- });
645
+ it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
646
+ const thenable = {then() {}};
647
+ function Foo({signal}) {
648
+ return (
649
+ <Suspense fallback="Loading...">
650
+ <Bar signal={signal} />
651
+ </Suspense>
652
+ );
653
+ }
654
620
- it('discards render phase updates if something suspends', () => {
621
- const thenable = {then() {}};
622
- function Foo({signal}) {
623
- return (
624
- <Suspense fallback="Loading...">
625
- <Bar signal={signal} />
626
- </Suspense>
627
- );
628
- }
655
+ let setLabel;
656
+ function Bar({signal: newSignal}) {
657
+ let [counter, setCounter] = useState(0);
658
630
- function Bar({signal: newSignal}) {
631
- let [counter, setCounter] = useState(0);
632
- let [signal, setSignal] = useState(true);
633
-
634
- // Increment a counter every time the signal changes
635
- if (signal !== newSignal) {
636
- setCounter(c => c + 1);
637
- setSignal(newSignal);
638
- if (counter === 0) {
639
- // We're suspending during a render that includes render phase
640
- // updates. Those updates should not persist to the next render.
641
- Scheduler.unstable_yieldValue('Suspend!');
642
- throw thenable;
643
- }
644
- }
659
+ if (counter === 1) {
660
+ // We're suspending during a render that includes render phase
661
+ // updates. Those updates should not persist to the next render.
662
+ Scheduler.unstable_yieldValue('Suspend!');
663
+ throw thenable;
664
+ }
665
646
- return <Text text={counter} />;
647
- }
666
+ let [signal, setSignal] = useState(true);
667
649
- const root = ReactNoop.createRoot();
650
- root.render(<Foo signal={true} />);
668
+ // Increment a counter every time the signal changes
669
+ if (signal !== newSignal) {
670
+ setCounter(c => c + 1);
671
+ setSignal(newSignal);
672
+ }
673
652
- expect(Scheduler).toFlushAndYield([0]);
653
- expect(root).toMatchRenderedOutput(<span prop={0} />);
674
+ let [label, _setLabel] = useState('A');
675
+ setLabel = _setLabel;
676
655
- root.render(<Foo signal={false} />);
656
- expect(Scheduler).toFlushAndYield(['Suspend!']);
657
- expect(root).toMatchRenderedOutput(<span prop={0} />);
677
+ return <Text text={`${label}:${counter}`} />;
678
+ }
679
659
- // Rendering again should suspend again.
660
- root.render(<Foo signal={false} />);
661
- expect(Scheduler).toFlushAndYield(['Suspend!']);
662
- });
680
+ const root = ReactNoop.createRoot();
681
+ root.render(<Foo signal={true} />);
682
664
- it('discards render phase updates if something suspends, but not other updates in the same component', async () => {
665
- const thenable = {then() {}};
666
- function Foo({signal}) {
667
- return (
668
- <Suspense fallback="Loading...">
669
- <Bar signal={signal} />
670
- </Suspense>
671
- );
672
- }
683
+ expect(Scheduler).toFlushAndYield(['A:0']);
684
+ expect(root).toMatchRenderedOutput(<span prop="A:0" />);
685
674
- let setLabel;
675
- function Bar({signal: newSignal}) {
676
- let [counter, setCounter] = useState(0);
686
+ await ReactNoop.act(async () => {
687
+ root.render(<Foo signal={false} />);
688
+ setLabel('B');
689
+ });
690
+ expect(Scheduler).toHaveYielded(['Suspend!']);
691
+ expect(root).toMatchRenderedOutput(<span prop="A:0" />);
692
+
693
+ // Rendering again should suspend again.
694
+ root.render(<Foo signal={false} />);
695
+ expect(Scheduler).toFlushAndYield(['Suspend!']);
696
+
697
+ // Flip the signal back to "cancel" the update. However, the update to
698
+ // label should still proceed. It shouldn't have been dropped.
699
+ root.render(<Foo signal={true} />);
700
+ expect(Scheduler).toFlushAndYield(['B:0']);
701
+ expect(root).toMatchRenderedOutput(<span prop="B:0" />);
702
+ });
703
678
- if (counter === 1) {
679
- // We're suspending during a render that includes render phase
680
- // updates. Those updates should not persist to the next render.
681
- Scheduler.unstable_yieldValue('Suspend!');
682
- throw thenable;
683
- }
704
+ // TODO: This should probably warn
705
+ it.experimental('calling startTransition inside render phase', async () => {
706
+ let startTransition;
707
+ function App() {
708
+ let [counter, setCounter] = useState(0);
709
+ let [_startTransition] = useTransition();
710
+ startTransition = _startTransition;
711
+
712
+ if (counter === 0) {
713
+ startTransition(() => {
714
+ setCounter(c => c + 1);
715
+ });
716
+ }
717
685
- let [signal, setSignal] = useState(true);
718
+ return <Text text={counter} />;
719
+ }
720
687
- // Increment a counter every time the signal changes
688
- if (signal !== newSignal) {
689
- setCounter(c => c + 1);
690
- setSignal(newSignal);
691
- }
721
+ const root = ReactNoop.createRoot();
722
+ root.render(<App />);
723
+ expect(Scheduler).toFlushAndYield([1]);
724
+ expect(root).toMatchRenderedOutput(<span prop={1} />);
725
+ });
726
+ });
727
693
- let [label, _setLabel] = useState('A');
694
- setLabel = _setLabel;
728
+ describe('useReducer', () => {
729
+ it('simple mount and update', () => {
730
+ const INCREMENT = 'INCREMENT';
731
+ const DECREMENT = 'DECREMENT';
732
+
733
+ function reducer(state, action) {
734
+ switch (action) {
735
+ case 'INCREMENT':
736
+ return state + 1;
737
+ case 'DECREMENT':
738
+ return state - 1;
739
+ default:
740
+ return state;
741
+ }
742
+ }
743
696
- return <Text text={`${label}:${counter}`} />;
697
- }
744
+ function Counter(props, ref) {
745
+ const [count, dispatch] = useReducer(reducer, 0);
746
+ useImperativeHandle(ref, () => ({dispatch}));
747
+ return <Text text={'Count: ' + count} />;
748
+ }
749
+ Counter = forwardRef(Counter);
750
+ const counter = React.createRef(null);
751
+ ReactNoop.render(<Counter ref={counter} />);
752
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
753
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
754
+
755
+ act(() => counter.current.dispatch(INCREMENT));
756
+ expect(Scheduler).toHaveYielded(['Count: 1']);
757
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
758
+ act(() => {
759
+ counter.current.dispatch(DECREMENT);
760
+ counter.current.dispatch(DECREMENT);
761
+ counter.current.dispatch(DECREMENT);
762
+ });
763
699
- const root = ReactNoop.createRoot();
700
- root.render(<Foo signal={true} />);
764
+ expect(Scheduler).toHaveYielded(['Count: -2']);
765
+ expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]);
766
+ });
767
702
- expect(Scheduler).toFlushAndYield(['A:0']);
703
- expect(root).toMatchRenderedOutput(<span prop="A:0" />);
768
+ it('lazy init', () => {
769
+ const INCREMENT = 'INCREMENT';
770
+ const DECREMENT = 'DECREMENT';
771
+
772
+ function reducer(state, action) {
773
+ switch (action) {
774
+ case 'INCREMENT':
775
+ return state + 1;
776
+ case 'DECREMENT':
777
+ return state - 1;
778
+ default:
779
+ return state;
780
+ }
781
+ }
782
705
- await ReactNoop.act(async () => {
706
- root.render(<Foo signal={false} />);
707
- setLabel('B');
708
- });
709
- expect(Scheduler).toHaveYielded(['Suspend!']);
710
- expect(root).toMatchRenderedOutput(<span prop="A:0" />);
711
-
712
- // Rendering again should suspend again.
713
- root.render(<Foo signal={false} />);
714
- expect(Scheduler).toFlushAndYield(['Suspend!']);
715
-
716
- // Flip the signal back to "cancel" the update. However, the update to
717
- // label should still proceed. It shouldn't have been dropped.
718
- root.render(<Foo signal={true} />);
719
- expect(Scheduler).toFlushAndYield(['B:0']);
720
- expect(root).toMatchRenderedOutput(<span prop="B:0" />);
783
+ function Counter(props, ref) {
784
+ const [count, dispatch] = useReducer(reducer, props, p => {
785
+ Scheduler.unstable_yieldValue('Init');
786
+ return p.initialCount;
787
});
788
+ useImperativeHandle(ref, () => ({dispatch}));
789
+ return <Text text={'Count: ' + count} />;
790
+ }
791
+ Counter = forwardRef(Counter);
792
+ const counter = React.createRef(null);
793
+ ReactNoop.render(<Counter initialCount={10} ref={counter} />);
794
+ expect(Scheduler).toFlushAndYield(['Init', 'Count: 10']);
795
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]);
796
+
797
+ act(() => counter.current.dispatch(INCREMENT));
798
+ expect(Scheduler).toHaveYielded(['Count: 11']);
799
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
800
+
801
+ act(() => {
802
+ counter.current.dispatch(DECREMENT);
803
+ counter.current.dispatch(DECREMENT);
804
+ counter.current.dispatch(DECREMENT);
805
+ });
806
723
- // TODO: This should probably warn
724
- it.experimental(
725
- 'calling startTransition inside render phase',
726
- async () => {
727
- let startTransition;
728
- function App() {
729
- let [counter, setCounter] = useState(0);
730
- let [_startTransition] = useTransition();
731
- startTransition = _startTransition;
732
-
733
- if (counter === 0) {
734
- startTransition(() => {
735
- setCounter(c => c + 1);
736
- });
737
- }
738
-
739
- return <Text text={counter} />;
740
- }
807
+ expect(Scheduler).toHaveYielded(['Count: 8']);
808
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]);
809
+ });
810
742
- const root = ReactNoop.createRoot();
743
- root.render(<App />);
744
- expect(Scheduler).toFlushAndYield([1]);
745
- expect(root).toMatchRenderedOutput(<span prop={1} />);
746
- },
747
- );
748
- });
811
+ // Regression test for https://github.com/facebook/react/issues/14360
812
+ it('handles dispatches with mixed priorities', () => {
813
+ const INCREMENT = 'INCREMENT';
814
750
- describe('useReducer', () => {
751
- it('simple mount and update', () => {
752
- const INCREMENT = 'INCREMENT';
753
- const DECREMENT = 'DECREMENT';
754
-
755
- function reducer(state, action) {
756
- switch (action) {
757
- case 'INCREMENT':
758
- return state + 1;
759
- case 'DECREMENT':
760
- return state - 1;
761
- default:
762
- return state;
763
- }
764
- }
815
+ function reducer(state, action) {
816
+ return action === INCREMENT ? state + 1 : state;
817
+ }
818
766
- function Counter(props, ref) {
767
- const [count, dispatch] = useReducer(reducer, 0);
768
- useImperativeHandle(ref, () => ({dispatch}));
769
- return <Text text={'Count: ' + count} />;
770
- }
771
- Counter = forwardRef(Counter);
772
- const counter = React.createRef(null);
773
- ReactNoop.render(<Counter ref={counter} />);
774
- expect(Scheduler).toFlushAndYield(['Count: 0']);
775
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
776
-
777
- act(() => counter.current.dispatch(INCREMENT));
778
- expect(Scheduler).toHaveYielded(['Count: 1']);
779
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
780
- act(() => {
781
- counter.current.dispatch(DECREMENT);
782
- counter.current.dispatch(DECREMENT);
783
- counter.current.dispatch(DECREMENT);
784
- });
819
+ function Counter(props, ref) {
820
+ const [count, dispatch] = useReducer(reducer, 0);
821
+ useImperativeHandle(ref, () => ({dispatch}));
822
+ return <Text text={'Count: ' + count} />;
823
+ }
824
786
- expect(Scheduler).toHaveYielded(['Count: -2']);
787
- expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]);
788
- });
825
+ Counter = forwardRef(Counter);
826
+ const counter = React.createRef(null);
827
+ ReactNoop.render(<Counter ref={counter} />);
828
790
- it('lazy init', () => {
791
- const INCREMENT = 'INCREMENT';
792
- const DECREMENT = 'DECREMENT';
793
-
794
- function reducer(state, action) {
795
- switch (action) {
796
- case 'INCREMENT':
797
- return state + 1;
798
- case 'DECREMENT':
799
- return state - 1;
800
- default:
801
- return state;
802
- }
803
- }
829
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
830
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
831
805
- function Counter(props, ref) {
806
- const [count, dispatch] = useReducer(reducer, props, p => {
807
- Scheduler.unstable_yieldValue('Init');
808
- return p.initialCount;
809
- });
810
- useImperativeHandle(ref, () => ({dispatch}));
811
- return <Text text={'Count: ' + count} />;
812
- }
813
- Counter = forwardRef(Counter);
814
- const counter = React.createRef(null);
815
- ReactNoop.render(<Counter initialCount={10} ref={counter} />);
816
- expect(Scheduler).toFlushAndYield(['Init', 'Count: 10']);
817
- expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]);
818
-
819
- act(() => counter.current.dispatch(INCREMENT));
820
- expect(Scheduler).toHaveYielded(['Count: 11']);
821
- expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]);
822
-
823
- act(() => {
824
- counter.current.dispatch(DECREMENT);
825
- counter.current.dispatch(DECREMENT);
826
- counter.current.dispatch(DECREMENT);
827
- });
832
+ ReactNoop.batchedUpdates(() => {
833
+ counter.current.dispatch(INCREMENT);
834
+ counter.current.dispatch(INCREMENT);
835
+ counter.current.dispatch(INCREMENT);
836
+ });
837
829
- expect(Scheduler).toHaveYielded(['Count: 8']);
830
- expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]);
831
- });
838
+ ReactNoop.flushSync(() => {
839
+ counter.current.dispatch(INCREMENT);
840
+ });
841
+ expect(Scheduler).toHaveYielded(['Count: 1']);
842
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
843
833
- // Regression test for https://github.com/facebook/react/issues/14360
834
- it('handles dispatches with mixed priorities', () => {
835
- const INCREMENT = 'INCREMENT';
844
+ expect(Scheduler).toFlushAndYield(['Count: 4']);
845
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
846
+ });
847
+ });
848
837
- function reducer(state, action) {
838
- return action === INCREMENT ? state + 1 : state;
839
- }
849
+ describe('useEffect', () => {
850
+ it('simple mount and update', () => {
851
+ function Counter(props) {
852
+ useEffect(() => {
853
+ Scheduler.unstable_yieldValue(`Passive effect [${props.count}]`);
854
+ });
855
+ return <Text text={'Count: ' + props.count} />;
856
+ }
857
+ act(() => {
858
+ ReactNoop.render(<Counter count={0} />, () =>
859
+ Scheduler.unstable_yieldValue('Sync effect'),
860
+ );
861
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
862
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
863
+ // Effects are deferred until after the commit
864
+ expect(Scheduler).toFlushAndYield(['Passive effect [0]']);
865
+ });
866
841
- function Counter(props, ref) {
842
- const [count, dispatch] = useReducer(reducer, 0);
843
- useImperativeHandle(ref, () => ({dispatch}));
844
- return <Text text={'Count: ' + count} />;
845
- }
867
+ act(() => {
868
+ ReactNoop.render(<Counter count={1} />, () =>
869
+ Scheduler.unstable_yieldValue('Sync effect'),
870
+ );
871
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
872
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
873
+ // Effects are deferred until after the commit
874
+ expect(Scheduler).toFlushAndYield(['Passive effect [1]']);
875
+ });
876
+ });
877
847
- Counter = forwardRef(Counter);
848
- const counter = React.createRef(null);
849
- ReactNoop.render(<Counter ref={counter} />);
878
+ it('flushes passive effects even with sibling deletions', () => {
879
+ function LayoutEffect(props) {
880
+ useLayoutEffect(() => {
881
+ Scheduler.unstable_yieldValue(`Layout effect`);
882
+ });
883
+ return <Text text="Layout" />;
884
+ }
885
+ function PassiveEffect(props) {
886
+ useEffect(() => {
887
+ Scheduler.unstable_yieldValue(`Passive effect`);
888
+ }, []);
889
+ return <Text text="Passive" />;
890
+ }
891
+ let passive = <PassiveEffect key="p" />;
892
+ act(() => {
893
+ ReactNoop.render([<LayoutEffect key="l" />, passive]);
894
+ expect(Scheduler).toFlushAndYieldThrough([
895
+ 'Layout',
896
+ 'Passive',
897
+ 'Layout effect',
898
+ ]);
899
+ expect(ReactNoop.getChildren()).toEqual([
900
+ span('Layout'),
901
+ span('Passive'),
902
+ ]);
903
+ // Destroying the first child shouldn't prevent the passive effect from
904
+ // being executed
905
+ ReactNoop.render([passive]);
906
+ expect(Scheduler).toFlushAndYield(['Passive effect']);
907
+ expect(ReactNoop.getChildren()).toEqual([span('Passive')]);
908
+ });
909
+ // exiting act calls flushPassiveEffects(), but there are none left to flush.
910
+ expect(Scheduler).toHaveYielded([]);
911
+ });
912
851
- expect(Scheduler).toFlushAndYield(['Count: 0']);
852
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
913
+ it('flushes passive effects even if siblings schedule an update', () => {
914
+ function PassiveEffect(props) {
915
+ useEffect(() => {
916
+ Scheduler.unstable_yieldValue('Passive effect');
917
+ });
918
+ return <Text text="Passive" />;
919
+ }
920
+ function LayoutEffect(props) {
921
+ let [count, setCount] = useState(0);
922
+ useLayoutEffect(() => {
923
+ // Scheduling work shouldn't interfere with the queued passive effect
924
+ if (count === 0) {
925
+ setCount(1);
926
+ }
927
+ Scheduler.unstable_yieldValue('Layout effect ' + count);
928
+ });
929
+ return <Text text="Layout" />;
930
+ }
931
854
- ReactNoop.batchedUpdates(() => {
855
- counter.current.dispatch(INCREMENT);
856
- counter.current.dispatch(INCREMENT);
857
- counter.current.dispatch(INCREMENT);
858
- });
932
+ ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);
933
860
- ReactNoop.flushSync(() => {
861
- counter.current.dispatch(INCREMENT);
862
- });
863
- expect(Scheduler).toHaveYielded(['Count: 1']);
864
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
934
+ act(() => {
935
+ expect(Scheduler).toFlushAndYield([
936
+ 'Passive',
937
+ 'Layout',
938
+ 'Layout effect 0',
939
+ 'Passive effect',
940
+ 'Layout',
941
+ 'Layout effect 1',
942
+ ]);
943
+ });
944
+
945
+ expect(ReactNoop.getChildren()).toEqual([
946
+ span('Passive'),
947
+ span('Layout'),
948
+ ]);
949
+ });
950
866
- expect(Scheduler).toFlushAndYield(['Count: 4']);
867
- expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]);
951
+ it('flushes passive effects even if siblings schedule a new root', () => {
952
+ function PassiveEffect(props) {
953
+ useEffect(() => {
954
+ Scheduler.unstable_yieldValue('Passive effect');
955
+ }, []);
956
+ return <Text text="Passive" />;
957
+ }
958
+ function LayoutEffect(props) {
959
+ useLayoutEffect(() => {
960
+ Scheduler.unstable_yieldValue('Layout effect');
961
+ // Scheduling work shouldn't interfere with the queued passive effect
962
+ ReactNoop.renderToRootWithID(<Text text="New Root" />, 'root2');
963
});
964
+ return <Text text="Layout" />;
965
+ }
966
+ act(() => {
967
+ ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);
968
+ expect(Scheduler).toFlushAndYield([
969
+ 'Passive',
970
+ 'Layout',
971
+ 'Layout effect',
972
+ 'Passive effect',
973
+ 'New Root',
974
+ ]);
975
+ expect(ReactNoop.getChildren()).toEqual([
976
+ span('Passive'),
977
+ span('Layout'),
978
+ ]);
979
});
980
+ });
981
871
- describe('useEffect', () => {
872
- it('simple mount and update', () => {
873
- function Counter(props) {
874
- useEffect(() => {
875
- Scheduler.unstable_yieldValue(`Passive effect [${props.count}]`);
876
- });
877
- return <Text text={'Count: ' + props.count} />;
982
+ it(
983
+ 'flushes effects serially by flushing old effects before flushing ' +
984
+ "new ones, if they haven't already fired",
985
+ () => {
986
+ function getCommittedText() {
987
+ const children = ReactNoop.getChildren();
988
+ if (children === null) {
989
+ return null;
990
}
879
- act(() => {
880
- ReactNoop.render(<Counter count={0} />, () =>
881
- Scheduler.unstable_yieldValue('Sync effect'),
882
- );
883
- expect(Scheduler).toFlushAndYieldThrough([
884
- 'Count: 0',
885
- 'Sync effect',
886
- ]);
887
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
888
- // Effects are deferred until after the commit
889
- expect(Scheduler).toFlushAndYield(['Passive effect [0]']);
890
- });
991
+ return children[0].prop;
992
+ }
993
892
- act(() => {
893
- ReactNoop.render(<Counter count={1} />, () =>
894
- Scheduler.unstable_yieldValue('Sync effect'),
994
+ function Counter(props) {
995
+ useEffect(() => {
996
+ Scheduler.unstable_yieldValue(
997
+ `Committed state when effect was fired: ${getCommittedText()}`,
998
);
896
- expect(Scheduler).toFlushAndYieldThrough([
897
- 'Count: 1',
898
- 'Sync effect',
899
- ]);
900
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
901
- // Effects are deferred until after the commit
902
- expect(Scheduler).toFlushAndYield(['Passive effect [1]']);
903
- });
904
- });
905
-
906
- it('flushes passive effects even with sibling deletions', () => {
907
- function LayoutEffect(props) {
908
- useLayoutEffect(() => {
909
- Scheduler.unstable_yieldValue(`Layout effect`);
910
- });
911
- return <Text text="Layout" />;
912
- }
913
- function PassiveEffect(props) {
914
- useEffect(() => {
915
- Scheduler.unstable_yieldValue(`Passive effect`);
916
- }, []);
917
- return <Text text="Passive" />;
918
- }
919
- let passive = <PassiveEffect key="p" />;
920
- act(() => {
921
- ReactNoop.render([<LayoutEffect key="l" />, passive]);
922
- expect(Scheduler).toFlushAndYieldThrough([
923
- 'Layout',
924
- 'Passive',
925
- 'Layout effect',
926
- ]);
927
- expect(ReactNoop.getChildren()).toEqual([
928
- span('Layout'),
929
- span('Passive'),
930
- ]);
931
- // Destroying the first child shouldn't prevent the passive effect from
932
- // being executed
933
- ReactNoop.render([passive]);
934
- expect(Scheduler).toFlushAndYield(['Passive effect']);
935
- expect(ReactNoop.getChildren()).toEqual([span('Passive')]);
999
});
937
- // exiting act calls flushPassiveEffects(), but there are none left to flush.
938
- expect(Scheduler).toHaveYielded([]);
1000
+ return <Text text={props.count} />;
1001
+ }
1002
+ act(() => {
1003
+ ReactNoop.render(<Counter count={0} />, () =>
1004
+ Scheduler.unstable_yieldValue('Sync effect'),
1005
+ );
1006
+ expect(Scheduler).toFlushAndYieldThrough([0, 'Sync effect']);
1007
+ expect(ReactNoop.getChildren()).toEqual([span(0)]);
1008
+ // Before the effects have a chance to flush, schedule another update
1009
+ ReactNoop.render(<Counter count={1} />, () =>
1010
+ Scheduler.unstable_yieldValue('Sync effect'),
1011
+ );
1012
+ expect(Scheduler).toFlushAndYieldThrough([
1013
+ // The previous effect flushes before the reconciliation
1014
+ 'Committed state when effect was fired: 0',
1015
+ 1,
1016
+ 'Sync effect',
1017
+ ]);
1018
+ expect(ReactNoop.getChildren()).toEqual([span(1)]);
1019
});
1020
941
- it('flushes passive effects even if siblings schedule an update', () => {
942
- function PassiveEffect(props) {
943
- useEffect(() => {
944
- Scheduler.unstable_yieldValue('Passive effect');
945
- });
946
- return <Text text="Passive" />;
947
- }
948
- function LayoutEffect(props) {
949
- let [count, setCount] = useState(0);
950
- useLayoutEffect(() => {
951
- // Scheduling work shouldn't interfere with the queued passive effect
952
- if (count === 0) {
953
- setCount(1);
954
- }
955
- Scheduler.unstable_yieldValue('Layout effect ' + count);
956
- });
957
- return <Text text="Layout" />;
958
- }
1021
+ expect(Scheduler).toHaveYielded([
1022
+ 'Committed state when effect was fired: 1',
1023
+ ]);
1024
+ },
1025
+ );
1026
+
1027
+ if (
1028
+ deferPassiveEffectCleanupDuringUnmount &&
1029
+ runAllPassiveEffectDestroysBeforeCreates
1030
+ ) {
1031
+ it('defers passive effect destroy functions during unmount', () => {
1032
+ function Child({bar, foo}) {
1033
+ React.useEffect(() => {
1034
+ Scheduler.unstable_yieldValue('passive bar create');
1035
+ return () => {
1036
+ Scheduler.unstable_yieldValue('passive bar destroy');
1037
+ };
1038
+ }, [bar]);
1039
+ React.useLayoutEffect(() => {
1040
+ Scheduler.unstable_yieldValue('layout bar create');
1041
+ return () => {
1042
+ Scheduler.unstable_yieldValue('layout bar destroy');
1043
+ };
1044
+ }, [bar]);
1045
+ React.useEffect(() => {
1046
+ Scheduler.unstable_yieldValue('passive foo create');
1047
+ return () => {
1048
+ Scheduler.unstable_yieldValue('passive foo destroy');
1049
+ };
1050
+ }, [foo]);
1051
+ React.useLayoutEffect(() => {
1052
+ Scheduler.unstable_yieldValue('layout foo create');
1053
+ return () => {
1054
+ Scheduler.unstable_yieldValue('layout foo destroy');
1055
+ };
1056
+ }, [foo]);
1057
+ Scheduler.unstable_yieldValue('render');
1058
+ return null;
1059
+ }
1060
960
- ReactNoop.render([
961
- <PassiveEffect key="p" />,
962
- <LayoutEffect key="l" />,
1061
+ act(() => {
1062
+ ReactNoop.render(<Child bar={1} foo={1} />, () =>
1063
+ Scheduler.unstable_yieldValue('Sync effect'),
1064
+ );
1065
+ expect(Scheduler).toFlushAndYieldThrough([
1066
+ 'render',
1067
+ 'layout bar create',
1068
+ 'layout foo create',
1069
+ 'Sync effect',
1070
]);
964
-
965
- act(() => {
966
- expect(Scheduler).toFlushAndYield([
967
- 'Passive',
968
- 'Layout',
969
- 'Layout effect 0',
970
- 'Passive effect',
971
- 'Layout',
972
- 'Layout effect 1',
973
- ]);
974
- });
975
-
976
- expect(ReactNoop.getChildren()).toEqual([
977
- span('Passive'),
978
- span('Layout'),
1071
+ // Effects are deferred until after the commit
1072
+ expect(Scheduler).toFlushAndYield([
1073
+ 'passive bar create',
1074
+ 'passive foo create',
1075
]);
1076
});
1077
982
- it('flushes passive effects even if siblings schedule a new root', () => {
983
- function PassiveEffect(props) {
984
- useEffect(() => {
985
- Scheduler.unstable_yieldValue('Passive effect');
986
- }, []);
987
- return <Text text="Passive" />;
988
- }
989
- function LayoutEffect(props) {
990
- useLayoutEffect(() => {
991
- Scheduler.unstable_yieldValue('Layout effect');
992
- // Scheduling work shouldn't interfere with the queued passive effect
993
- ReactNoop.renderToRootWithID(<Text text="New Root" />, 'root2');
994
- });
995
- return <Text text="Layout" />;
996
- }
997
- act(() => {
998
- ReactNoop.render([
999
- <PassiveEffect key="p" />,
1000
- <LayoutEffect key="l" />,
1001
- ]);
1002
- expect(Scheduler).toFlushAndYield([
1003
- 'Passive',
1004
- 'Layout',
1005
- 'Layout effect',
1006
- 'Passive effect',
1007
- 'New Root',
1008
- ]);
1009
- expect(ReactNoop.getChildren()).toEqual([
1010
- span('Passive'),
1011
- span('Layout'),
1012
- ]);
1013
- });
1078
+ // This update is exists to test an internal implementation detail:
1079
+ // Effects without updating dependencies lose their layout/passive tag during an update.
1080
+ act(() => {
1081
+ ReactNoop.render(<Child bar={1} foo={2} />, () =>
1082
+ Scheduler.unstable_yieldValue('Sync effect'),
1083
+ );
1084
+ expect(Scheduler).toFlushAndYieldThrough([
1085
+ 'render',
1086
+ 'layout foo destroy',
1087
+ 'layout foo create',
1088
+ 'Sync effect',
1089
+ ]);
1090
+ // Effects are deferred until after the commit
1091
+ expect(Scheduler).toFlushAndYield([
1092
+ 'passive foo destroy',
1093
+ 'passive foo create',
1094
+ ]);
1095
});
1096
1016
- it(
1017
- 'flushes effects serially by flushing old effects before flushing ' +
1018
- "new ones, if they haven't already fired",
1019
- () => {
1020
- function getCommittedText() {
1021
- const children = ReactNoop.getChildren();
1022
- if (children === null) {
1023
- return null;
1024
- }
1025
- return children[0].prop;
1026
- }
1027
-
1028
- function Counter(props) {
1029
- useEffect(() => {
1030
- Scheduler.unstable_yieldValue(
1031
- `Committed state when effect was fired: ${getCommittedText()}`,
1032
- );
1033
- });
1034
- return <Text text={props.count} />;
1035
- }
1036
- act(() => {
1037
- ReactNoop.render(<Counter count={0} />, () =>
1038
- Scheduler.unstable_yieldValue('Sync effect'),
1039
- );
1040
- expect(Scheduler).toFlushAndYieldThrough([0, 'Sync effect']);
1041
- expect(ReactNoop.getChildren()).toEqual([span(0)]);
1042
- // Before the effects have a chance to flush, schedule another update
1043
- ReactNoop.render(<Counter count={1} />, () =>
1044
- Scheduler.unstable_yieldValue('Sync effect'),
1045
- );
1046
- expect(Scheduler).toFlushAndYieldThrough([
1047
- // The previous effect flushes before the reconciliation
1048
- 'Committed state when effect was fired: 0',
1049
- 1,
1050
- 'Sync effect',
1051
- ]);
1052
- expect(ReactNoop.getChildren()).toEqual([span(1)]);
1053
- });
1054
-
1055
- expect(Scheduler).toHaveYielded([
1056
- 'Committed state when effect was fired: 1',
1057
- ]);
1058
- },
1059
- );
1060
-
1061
- if (
1062
- deferPassiveEffectCleanupDuringUnmount &&
1063
- runAllPassiveEffectDestroysBeforeCreates
1064
- ) {
1065
- it('defers passive effect destroy functions during unmount', () => {
1066
- function Child({bar, foo}) {
1067
- React.useEffect(() => {
1068
- Scheduler.unstable_yieldValue('passive bar create');
1069
- return () => {
1070
- Scheduler.unstable_yieldValue('passive bar destroy');
1071
- };
1072
- }, [bar]);
1073
- React.useLayoutEffect(() => {
1074
- Scheduler.unstable_yieldValue('layout bar create');
1075
- return () => {
1076
- Scheduler.unstable_yieldValue('layout bar destroy');
1077
- };
1078
- }, [bar]);
1079
- React.useEffect(() => {
1080
- Scheduler.unstable_yieldValue('passive foo create');
1081
- return () => {
1082
- Scheduler.unstable_yieldValue('passive foo destroy');
1083
- };
1084
- }, [foo]);
1085
- React.useLayoutEffect(() => {
1086
- Scheduler.unstable_yieldValue('layout foo create');
1087
- return () => {
1088
- Scheduler.unstable_yieldValue('layout foo destroy');
1089
- };
1090
- }, [foo]);
1091
- Scheduler.unstable_yieldValue('render');
1092
- return null;
1093
- }
1094
-
1095
- act(() => {
1096
- ReactNoop.render(<Child bar={1} foo={1} />, () =>
1097
- Scheduler.unstable_yieldValue('Sync effect'),
1098
- );
1099
- expect(Scheduler).toFlushAndYieldThrough([
1100
- 'render',
1101
- 'layout bar create',
1102
- 'layout foo create',
1103
- 'Sync effect',
1104
- ]);
1105
- // Effects are deferred until after the commit
1106
- expect(Scheduler).toFlushAndYield([
1107
- 'passive bar create',
1108
- 'passive foo create',
1109
- ]);
1110
- });
1097
+ // Unmount the component and verify that passive destroy functions are deferred until post-commit.
1098
+ act(() => {
1099
+ ReactNoop.render(null, () =>
1100
+ Scheduler.unstable_yieldValue('Sync effect'),
1101
+ );
1102
+ expect(Scheduler).toFlushAndYieldThrough([
1103
+ 'layout bar destroy',
1104
+ 'layout foo destroy',
1105
+ 'Sync effect',
1106
+ ]);
1107
+ // Effects are deferred until after the commit
1108
+ expect(Scheduler).toFlushAndYield([
1109
+ 'passive bar destroy',
1110
+ 'passive foo destroy',
1111
+ ]);
1112
+ });
1113
+ });
1114
1112
- // This update is exists to test an internal implementation detail:
1113
- // Effects without updating dependencies lose their layout/passive tag during an update.
1114
- act(() => {
1115
- ReactNoop.render(<Child bar={1} foo={2} />, () =>
1116
- Scheduler.unstable_yieldValue('Sync effect'),
1117
- );
1118
- expect(Scheduler).toFlushAndYieldThrough([
1119
- 'render',
1120
- 'layout foo destroy',
1121
- 'layout foo create',
1122
- 'Sync effect',
1123
- ]);
1124
- // Effects are deferred until after the commit
1125
- expect(Scheduler).toFlushAndYield([
1126
- 'passive foo destroy',
1127
- 'passive foo create',
1128
- ]);
1129
- });
1115
+ it('does not warn about state updates for unmounted components with pending passive unmounts', () => {
1116
+ let completePendingRequest = null;
1117
+ function Component() {
1118
+ Scheduler.unstable_yieldValue('Component');
1119
+ const [didLoad, setDidLoad] = React.useState(false);
1120
+ React.useLayoutEffect(() => {
1121
+ Scheduler.unstable_yieldValue('layout create');
1122
+ return () => {
1123
+ Scheduler.unstable_yieldValue('layout destroy');
1124
+ };
1125
+ }, []);
1126
+ React.useEffect(() => {
1127
+ Scheduler.unstable_yieldValue('passive create');
1128
+ // Mimic an XHR request with a complete handler that updates state.
1129
+ completePendingRequest = () => setDidLoad(true);
1130
+ return () => {
1131
+ Scheduler.unstable_yieldValue('passive destroy');
1132
+ };
1133
+ }, []);
1134
+ return didLoad;
1135
+ }
1136
1131
- // Unmount the component and verify that passive destroy functions are deferred until post-commit.
1132
- act(() => {
1133
- ReactNoop.render(null, () =>
1134
- Scheduler.unstable_yieldValue('Sync effect'),
1135
- );
1136
- expect(Scheduler).toFlushAndYieldThrough([
1137
- 'layout bar destroy',
1138
- 'layout foo destroy',
1139
- 'Sync effect',
1140
- ]);
1141
- // Effects are deferred until after the commit
1142
- expect(Scheduler).toFlushAndYield([
1143
- 'passive bar destroy',
1144
- 'passive foo destroy',
1145
- ]);
1146
- });
1147
- });
1137
+ act(() => {
1138
+ ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1139
+ Scheduler.unstable_yieldValue('Sync effect'),
1140
+ );
1141
+ expect(Scheduler).toFlushAndYieldThrough([
1142
+ 'Component',
1143
+ 'layout create',
1144
+ 'Sync effect',
1145
+ ]);
1146
+ ReactNoop.flushPassiveEffects();
1147
+ expect(Scheduler).toHaveYielded(['passive create']);
1148
1149
- it('does not warn about state updates for unmounted components with pending passive unmounts', () => {
1150
- let completePendingRequest = null;
1151
- function Component() {
1152
- Scheduler.unstable_yieldValue('Component');
1153
- const [didLoad, setDidLoad] = React.useState(false);
1154
- React.useLayoutEffect(() => {
1155
- Scheduler.unstable_yieldValue('layout create');
1156
- return () => {
1157
- Scheduler.unstable_yieldValue('layout destroy');
1158
- };
1159
- }, []);
1160
- React.useEffect(() => {
1161
- Scheduler.unstable_yieldValue('passive create');
1162
- // Mimic an XHR request with a complete handler that updates state.
1163
- completePendingRequest = () => setDidLoad(true);
1164
- return () => {
1165
- Scheduler.unstable_yieldValue('passive destroy');
1166
- };
1167
- }, []);
1168
- return didLoad;
1169
- }
1149
+ // Unmount but don't process pending passive destroy function
1150
+ ReactNoop.unmountRootWithID('root');
1151
+ expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
1152
1171
- act(() => {
1172
- ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1173
- Scheduler.unstable_yieldValue('Sync effect'),
1174
- );
1175
- expect(Scheduler).toFlushAndYieldThrough([
1176
- 'Component',
1177
- 'layout create',
1178
- 'Sync effect',
1179
- ]);
1180
- ReactNoop.flushPassiveEffects();
1181
- expect(Scheduler).toHaveYielded(['passive create']);
1182
-
1183
- // Unmount but don't process pending passive destroy function
1184
- ReactNoop.unmountRootWithID('root');
1185
- expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
1186
-
1187
- // Simulate an XHR completing, which will cause a state update-
1188
- // but should not log a warning.
1189
- completePendingRequest();
1190
-
1191
- ReactNoop.flushPassiveEffects();
1192
- expect(Scheduler).toHaveYielded(['passive destroy']);
1193
- });
1194
- });
1153
+ // Simulate an XHR completing, which will cause a state update-
1154
+ // but should not log a warning.
1155
+ completePendingRequest();
1156
1196
- it('still warns about state updates for unmounted components with no pending passive unmounts', () => {
1197
- let completePendingRequest = null;
1198
- function Component() {
1199
- Scheduler.unstable_yieldValue('Component');
1200
- const [didLoad, setDidLoad] = React.useState(false);
1201
- React.useLayoutEffect(() => {
1202
- Scheduler.unstable_yieldValue('layout create');
1203
- // Mimic an XHR request with a complete handler that updates state.
1204
- completePendingRequest = () => setDidLoad(true);
1205
- return () => {
1206
- Scheduler.unstable_yieldValue('layout destroy');
1207
- };
1208
- }, []);
1209
- return didLoad;
1210
- }
1157
+ ReactNoop.flushPassiveEffects();
1158
+ expect(Scheduler).toHaveYielded(['passive destroy']);
1159
+ });
1160
+ });
1161
1212
- act(() => {
1213
- ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1214
- Scheduler.unstable_yieldValue('Sync effect'),
1215
- );
1216
- expect(Scheduler).toFlushAndYieldThrough([
1217
- 'Component',
1218
- 'layout create',
1219
- 'Sync effect',
1220
- ]);
1221
-
1222
- // Unmount but don't process pending passive destroy function
1223
- ReactNoop.unmountRootWithID('root');
1224
- expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
1225
-
1226
- // Simulate an XHR completing.
1227
- expect(completePendingRequest).toErrorDev(
1228
- "Warning: Can't perform a React state update on an unmounted component.",
1229
- );
1230
- });
1231
- });
1162
+ it('still warns about state updates for unmounted components with no pending passive unmounts', () => {
1163
+ let completePendingRequest = null;
1164
+ function Component() {
1165
+ Scheduler.unstable_yieldValue('Component');
1166
+ const [didLoad, setDidLoad] = React.useState(false);
1167
+ React.useLayoutEffect(() => {
1168
+ Scheduler.unstable_yieldValue('layout create');
1169
+ // Mimic an XHR request with a complete handler that updates state.
1170
+ completePendingRequest = () => setDidLoad(true);
1171
+ return () => {
1172
+ Scheduler.unstable_yieldValue('layout destroy');
1173
+ };
1174
+ }, []);
1175
+ return didLoad;
1176
+ }
1177
1233
- it('still warns if there are pending passive unmount effects but not for the current fiber', () => {
1234
- let completePendingRequest = null;
1235
- function ComponentWithXHR() {
1236
- Scheduler.unstable_yieldValue('Component');
1237
- const [didLoad, setDidLoad] = React.useState(false);
1238
- React.useLayoutEffect(() => {
1239
- Scheduler.unstable_yieldValue('a:layout create');
1240
- return () => {
1241
- Scheduler.unstable_yieldValue('a:layout destroy');
1242
- };
1243
- }, []);
1244
- React.useEffect(() => {
1245
- Scheduler.unstable_yieldValue('a:passive create');
1246
- // Mimic an XHR request with a complete handler that updates state.
1247
- completePendingRequest = () => setDidLoad(true);
1248
- }, []);
1249
- return didLoad;
1250
- }
1178
+ act(() => {
1179
+ ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1180
+ Scheduler.unstable_yieldValue('Sync effect'),
1181
+ );
1182
+ expect(Scheduler).toFlushAndYieldThrough([
1183
+ 'Component',
1184
+ 'layout create',
1185
+ 'Sync effect',
1186
+ ]);
1187
1252
- function ComponentWithPendingPassiveUnmount() {
1253
- React.useEffect(() => {
1254
- Scheduler.unstable_yieldValue('b:passive create');
1255
- return () => {
1256
- Scheduler.unstable_yieldValue('b:passive destroy');
1257
- };
1258
- }, []);
1259
- return null;
1260
- }
1188
+ // Unmount but don't process pending passive destroy function
1189
+ ReactNoop.unmountRootWithID('root');
1190
+ expect(Scheduler).toFlushAndYieldThrough(['layout destroy']);
1191
1262
- act(() => {
1263
- ReactNoop.renderToRootWithID(
1264
- <>
1265
- <ComponentWithXHR />
1266
- <ComponentWithPendingPassiveUnmount />
1267
- </>,
1268
- 'root',
1269
- () => Scheduler.unstable_yieldValue('Sync effect'),
1270
- );
1271
- expect(Scheduler).toFlushAndYieldThrough([
1272
- 'Component',
1273
- 'a:layout create',
1274
- 'Sync effect',
1275
- ]);
1276
- ReactNoop.flushPassiveEffects();
1277
- expect(Scheduler).toHaveYielded([
1278
- 'a:passive create',
1279
- 'b:passive create',
1280
- ]);
1281
-
1282
- // Unmount but don't process pending passive destroy function
1283
- ReactNoop.unmountRootWithID('root');
1284
- expect(Scheduler).toFlushAndYieldThrough(['a:layout destroy']);
1285
-
1286
- // Simulate an XHR completing in the component without a pending passive effect..
1287
- expect(completePendingRequest).toErrorDev(
1288
- "Warning: Can't perform a React state update on an unmounted component.",
1289
- );
1290
- });
1291
- });
1192
+ // Simulate an XHR completing.
1193
+ expect(completePendingRequest).toErrorDev(
1194
+ "Warning: Can't perform a React state update on an unmounted component.",
1195
+ );
1196
+ });
1197
+ });
1198
1293
- it('still warns about state updates from within passive unmount function', () => {
1294
- function Component() {
1295
- Scheduler.unstable_yieldValue('Component');
1296
- const [didLoad, setDidLoad] = React.useState(false);
1297
- React.useEffect(() => {
1298
- Scheduler.unstable_yieldValue('passive create');
1299
- return () => {
1300
- setDidLoad(true);
1301
- Scheduler.unstable_yieldValue('passive destroy');
1302
- };
1303
- }, []);
1304
- return didLoad;
1305
- }
1199
+ it('still warns if there are pending passive unmount effects but not for the current fiber', () => {
1200
+ let completePendingRequest = null;
1201
+ function ComponentWithXHR() {
1202
+ Scheduler.unstable_yieldValue('Component');
1203
+ const [didLoad, setDidLoad] = React.useState(false);
1204
+ React.useLayoutEffect(() => {
1205
+ Scheduler.unstable_yieldValue('a:layout create');
1206
+ return () => {
1207
+ Scheduler.unstable_yieldValue('a:layout destroy');
1208
+ };
1209
+ }, []);
1210
+ React.useEffect(() => {
1211
+ Scheduler.unstable_yieldValue('a:passive create');
1212
+ // Mimic an XHR request with a complete handler that updates state.
1213
+ completePendingRequest = () => setDidLoad(true);
1214
+ }, []);
1215
+ return didLoad;
1216
+ }
1217
1307
- act(() => {
1308
- ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1309
- Scheduler.unstable_yieldValue('Sync effect'),
1310
- );
1311
- expect(Scheduler).toFlushAndYieldThrough([
1312
- 'Component',
1313
- 'Sync effect',
1314
- 'passive create',
1315
- ]);
1316
-
1317
- // Unmount but don't process pending passive destroy function
1318
- ReactNoop.unmountRootWithID('root');
1319
- expect(() => {
1320
- expect(Scheduler).toFlushAndYield(['passive destroy']);
1321
- }).toErrorDev(
1322
- "Warning: Can't perform a React state update on an unmounted component.",
1323
- );
1324
- });
1325
- });
1218
+ function ComponentWithPendingPassiveUnmount() {
1219
+ React.useEffect(() => {
1220
+ Scheduler.unstable_yieldValue('b:passive create');
1221
+ return () => {
1222
+ Scheduler.unstable_yieldValue('b:passive destroy');
1223
+ };
1224
+ }, []);
1225
+ return null;
1226
}
1227
1328
- it('updates have async priority', () => {
1329
- function Counter(props) {
1330
- const [count, updateCount] = useState('(empty)');
1331
- useEffect(() => {
1332
- Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1333
- updateCount(props.count);
1334
- }, [props.count]);
1335
- return <Text text={'Count: ' + count} />;
1336
- }
1337
- act(() => {
1338
- ReactNoop.render(<Counter count={0} />, () =>
1339
- Scheduler.unstable_yieldValue('Sync effect'),
1340
- );
1341
- expect(Scheduler).toFlushAndYieldThrough([
1342
- 'Count: (empty)',
1343
- 'Sync effect',
1344
- ]);
1345
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1346
- ReactNoop.flushPassiveEffects();
1347
- expect(Scheduler).toHaveYielded(['Schedule update [0]']);
1348
- expect(Scheduler).toFlushAndYield(['Count: 0']);
1349
- });
1228
+ act(() => {
1229
+ ReactNoop.renderToRootWithID(
1230
+ <>
1231
+ <ComponentWithXHR />
1232
+ <ComponentWithPendingPassiveUnmount />
1233
+ </>,
1234
+ 'root',
1235
+ () => Scheduler.unstable_yieldValue('Sync effect'),
1236
+ );
1237
+ expect(Scheduler).toFlushAndYieldThrough([
1238
+ 'Component',
1239
+ 'a:layout create',
1240
+ 'Sync effect',
1241
+ ]);
1242
+ ReactNoop.flushPassiveEffects();
1243
+ expect(Scheduler).toHaveYielded([
1244
+ 'a:passive create',
1245
+ 'b:passive create',
1246
+ ]);
1247
1351
- act(() => {
1352
- ReactNoop.render(<Counter count={1} />, () =>
1353
- Scheduler.unstable_yieldValue('Sync effect'),
1354
- );
1355
- expect(Scheduler).toFlushAndYieldThrough([
1356
- 'Count: 0',
1357
- 'Sync effect',
1358
- ]);
1359
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1360
- ReactNoop.flushPassiveEffects();
1361
- expect(Scheduler).toHaveYielded(['Schedule update [1]']);
1362
- expect(Scheduler).toFlushAndYield(['Count: 1']);
1363
- });
1364
- });
1248
+ // Unmount but don't process pending passive destroy function
1249
+ ReactNoop.unmountRootWithID('root');
1250
+ expect(Scheduler).toFlushAndYieldThrough(['a:layout destroy']);
1251
1366
- it('updates have async priority even if effects are flushed early', () => {
1367
- function Counter(props) {
1368
- const [count, updateCount] = useState('(empty)');
1369
- useEffect(() => {
1370
- Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1371
- updateCount(props.count);
1372
- }, [props.count]);
1373
- return <Text text={'Count: ' + count} />;
1374
- }
1375
- act(() => {
1376
- ReactNoop.render(<Counter count={0} />, () =>
1377
- Scheduler.unstable_yieldValue('Sync effect'),
1378
- );
1379
- expect(Scheduler).toFlushAndYieldThrough([
1380
- 'Count: (empty)',
1381
- 'Sync effect',
1382
- ]);
1383
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1384
-
1385
- // Rendering again should flush the previous commit's effects
1386
- ReactNoop.render(<Counter count={1} />, () =>
1387
- Scheduler.unstable_yieldValue('Sync effect'),
1388
- );
1389
- expect(Scheduler).toFlushAndYieldThrough([
1390
- 'Schedule update [0]',
1391
- 'Count: 0',
1392
- ]);
1393
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1394
-
1395
- expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
1396
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1397
- ReactNoop.flushPassiveEffects();
1398
- expect(Scheduler).toHaveYielded(['Schedule update [1]']);
1399
- expect(Scheduler).toFlushAndYield(['Count: 1']);
1400
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1401
- });
1252
+ // Simulate an XHR completing in the component without a pending passive effect..
1253
+ expect(completePendingRequest).toErrorDev(
1254
+ "Warning: Can't perform a React state update on an unmounted component.",
1255
+ );
1256
});
1257
+ });
1258
1404
- it('flushes passive effects when flushing discrete updates', () => {
1405
- let _updateCount;
1406
- function Counter(props) {
1407
- const [count, updateCount] = useState(0);
1408
- _updateCount = updateCount;
1409
- useEffect(() => {
1410
- Scheduler.unstable_yieldValue(`Will set count to 1`);
1411
- updateCount(1);
1412
- }, []);
1413
- return <Text text={'Count: ' + count} />;
1414
- }
1259
+ it('still warns about state updates from within passive unmount function', () => {
1260
+ function Component() {
1261
+ Scheduler.unstable_yieldValue('Component');
1262
+ const [didLoad, setDidLoad] = React.useState(false);
1263
+ React.useEffect(() => {
1264
+ Scheduler.unstable_yieldValue('passive create');
1265
+ return () => {
1266
+ setDidLoad(true);
1267
+ Scheduler.unstable_yieldValue('passive destroy');
1268
+ };
1269
+ }, []);
1270
+ return didLoad;
1271
+ }
1272
1416
- // we explicitly wait for missing act() warnings here since
1417
- // it's a lot harder to simulate this condition inside an act scope
1418
- expect(() => {
1419
- ReactNoop.render(<Counter count={0} />, () =>
1420
- Scheduler.unstable_yieldValue('Sync effect'),
1421
- );
1422
- expect(Scheduler).toFlushAndYieldThrough([
1423
- 'Count: 0',
1424
- 'Sync effect',
1425
- ]);
1426
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1427
- }).toErrorDev(['An update to Counter ran an effect']);
1428
-
1429
- // A discrete event forces the passive effect to be flushed --
1430
- // updateCount(1) happens first, so 2 wins.
1431
- ReactNoop.flushDiscreteUpdates();
1432
- ReactNoop.discreteUpdates(() => {
1433
- // (use batchedUpdates to silence the act() warning)
1434
- ReactNoop.batchedUpdates(() => {
1435
- _updateCount(2);
1436
- });
1437
- });
1438
- expect(Scheduler).toHaveYielded(['Will set count to 1']);
1439
- expect(() => {
1440
- expect(Scheduler).toFlushAndYield(['Count: 2']);
1441
- }).toErrorDev([
1442
- 'An update to Counter ran an effect',
1443
- 'An update to Counter ran an effect',
1273
+ act(() => {
1274
+ ReactNoop.renderToRootWithID(<Component />, 'root', () =>
1275
+ Scheduler.unstable_yieldValue('Sync effect'),
1276
+ );
1277
+ expect(Scheduler).toFlushAndYieldThrough([
1278
+ 'Component',
1279
+ 'Sync effect',
1280
+ 'passive create',
1281
]);
1282
1446
- expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
1283
+ // Unmount but don't process pending passive destroy function
1284
+ ReactNoop.unmountRootWithID('root');
1285
+ expect(() => {
1286
+ expect(Scheduler).toFlushAndYield(['passive destroy']);
1287
+ }).toErrorDev(
1288
+ "Warning: Can't perform a React state update on an unmounted component.",
1289
+ );
1290
});
1291
+ });
1292
+ }
1293
+
1294
+ it('updates have async priority', () => {
1295
+ function Counter(props) {
1296
+ const [count, updateCount] = useState('(empty)');
1297
+ useEffect(() => {
1298
+ Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1299
+ updateCount(props.count);
1300
+ }, [props.count]);
1301
+ return <Text text={'Count: ' + count} />;
1302
+ }
1303
+ act(() => {
1304
+ ReactNoop.render(<Counter count={0} />, () =>
1305
+ Scheduler.unstable_yieldValue('Sync effect'),
1306
+ );
1307
+ expect(Scheduler).toFlushAndYieldThrough([
1308
+ 'Count: (empty)',
1309
+ 'Sync effect',
1310
+ ]);
1311
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1312
+ ReactNoop.flushPassiveEffects();
1313
+ expect(Scheduler).toHaveYielded(['Schedule update [0]']);
1314
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
1315
+ });
1316
1449
- it('flushes passive effects when flushing discrete updates (with tracing)', () => {
1450
- const onInteractionScheduledWorkCompleted = jest.fn();
1451
- const onWorkCanceled = jest.fn();
1452
- SchedulerTracing.unstable_subscribe({
1453
- onInteractionScheduledWorkCompleted,
1454
- onInteractionTraced: jest.fn(),
1455
- onWorkCanceled,
1456
- onWorkScheduled: jest.fn(),
1457
- onWorkStarted: jest.fn(),
1458
- onWorkStopped: jest.fn(),
1459
- });
1460
-
1461
- let _updateCount;
1462
- function Counter(props) {
1463
- const [count, updateCount] = useState(0);
1464
- _updateCount = updateCount;
1465
- useEffect(() => {
1466
- expect(
1467
- SchedulerTracing.unstable_getCurrent(),
1468
- ).toMatchInteractions([tracingEvent]);
1469
- Scheduler.unstable_yieldValue(`Will set count to 1`);
1470
- updateCount(1);
1471
- }, []);
1472
- return <Text text={'Count: ' + count} />;
1473
- }
1317
+ act(() => {
1318
+ ReactNoop.render(<Counter count={1} />, () =>
1319
+ Scheduler.unstable_yieldValue('Sync effect'),
1320
+ );
1321
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1322
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1323
+ ReactNoop.flushPassiveEffects();
1324
+ expect(Scheduler).toHaveYielded(['Schedule update [1]']);
1325
+ expect(Scheduler).toFlushAndYield(['Count: 1']);
1326
+ });
1327
+ });
1328
1475
- const tracingEvent = {id: 0, name: 'hello', timestamp: 0};
1476
- // we explicitly wait for missing act() warnings here since
1477
- // it's a lot harder to simulate this condition inside an act scope
1478
- expect(() => {
1479
- SchedulerTracing.unstable_trace(
1480
- tracingEvent.name,
1481
- tracingEvent.timestamp,
1482
- () => {
1483
- ReactNoop.render(<Counter count={0} />, () =>
1484
- Scheduler.unstable_yieldValue('Sync effect'),
1485
- );
1486
- },
1487
- );
1488
- expect(Scheduler).toFlushAndYieldThrough([
1489
- 'Count: 0',
1490
- 'Sync effect',
1491
- ]);
1492
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1493
- }).toErrorDev(['An update to Counter ran an effect']);
1494
-
1495
- expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0);
1496
-
1497
- // A discrete event forces the passive effect to be flushed --
1498
- // updateCount(1) happens first, so 2 wins.
1499
- ReactNoop.flushDiscreteUpdates();
1500
- ReactNoop.discreteUpdates(() => {
1501
- // (use batchedUpdates to silence the act() warning)
1502
- ReactNoop.batchedUpdates(() => {
1503
- _updateCount(2);
1504
- });
1505
- });
1506
- expect(Scheduler).toHaveYielded(['Will set count to 1']);
1507
- expect(() => {
1508
- expect(Scheduler).toFlushAndYield(['Count: 2']);
1509
- }).toErrorDev([
1510
- 'An update to Counter ran an effect',
1511
- 'An update to Counter ran an effect',
1512
- ]);
1329
+ it('updates have async priority even if effects are flushed early', () => {
1330
+ function Counter(props) {
1331
+ const [count, updateCount] = useState('(empty)');
1332
+ useEffect(() => {
1333
+ Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1334
+ updateCount(props.count);
1335
+ }, [props.count]);
1336
+ return <Text text={'Count: ' + count} />;
1337
+ }
1338
+ act(() => {
1339
+ ReactNoop.render(<Counter count={0} />, () =>
1340
+ Scheduler.unstable_yieldValue('Sync effect'),
1341
+ );
1342
+ expect(Scheduler).toFlushAndYieldThrough([
1343
+ 'Count: (empty)',
1344
+ 'Sync effect',
1345
+ ]);
1346
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1347
1514
- expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
1348
+ // Rendering again should flush the previous commit's effects
1349
+ ReactNoop.render(<Counter count={1} />, () =>
1350
+ Scheduler.unstable_yieldValue('Sync effect'),
1351
+ );
1352
+ expect(Scheduler).toFlushAndYieldThrough([
1353
+ 'Schedule update [0]',
1354
+ 'Count: 0',
1355
+ ]);
1356
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1357
1516
- expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1517
- expect(onWorkCanceled).toHaveBeenCalledTimes(0);
1518
- });
1358
+ expect(Scheduler).toFlushAndYieldThrough(['Sync effect']);
1359
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1360
+ ReactNoop.flushPassiveEffects();
1361
+ expect(Scheduler).toHaveYielded(['Schedule update [1]']);
1362
+ expect(Scheduler).toFlushAndYield(['Count: 1']);
1363
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1364
+ });
1365
+ });
1366
1520
- it(
1521
- 'in legacy mode, useEffect is deferred and updates finish synchronously ' +
1522
- '(in a single batch)',
1523
- () => {
1524
- function Counter(props) {
1525
- const [count, updateCount] = useState('(empty)');
1526
- useEffect(() => {
1527
- // Update multiple times. These should all be batched together in
1528
- // a single render.
1529
- updateCount(props.count);
1530
- updateCount(props.count);
1531
- updateCount(props.count);
1532
- updateCount(props.count);
1533
- updateCount(props.count);
1534
- updateCount(props.count);
1535
- }, [props.count]);
1536
- return <Text text={'Count: ' + count} />;
1537
- }
1538
- act(() => {
1539
- ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
1540
- // Even in legacy mode, effects are deferred until after paint
1541
- expect(Scheduler).toFlushAndYieldThrough(['Count: (empty)']);
1542
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1543
- });
1367
+ it('flushes passive effects when flushing discrete updates', () => {
1368
+ let _updateCount;
1369
+ function Counter(props) {
1370
+ const [count, updateCount] = useState(0);
1371
+ _updateCount = updateCount;
1372
+ useEffect(() => {
1373
+ Scheduler.unstable_yieldValue(`Will set count to 1`);
1374
+ updateCount(1);
1375
+ }, []);
1376
+ return <Text text={'Count: ' + count} />;
1377
+ }
1378
1545
- // effects get fored on exiting act()
1546
- // There were multiple updates, but there should only be a
1547
- // single render
1548
- expect(Scheduler).toHaveYielded(['Count: 0']);
1549
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1550
- },
1379
+ // we explicitly wait for missing act() warnings here since
1380
+ // it's a lot harder to simulate this condition inside an act scope
1381
+ expect(() => {
1382
+ ReactNoop.render(<Counter count={0} />, () =>
1383
+ Scheduler.unstable_yieldValue('Sync effect'),
1384
);
1385
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1386
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1387
+ }).toErrorDev(['An update to Counter ran an effect']);
1388
1553
- it('flushSync is not allowed', () => {
1554
- function Counter(props) {
1555
- const [count, updateCount] = useState('(empty)');
1556
- useEffect(() => {
1557
- Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1558
- ReactNoop.flushSync(() => {
1559
- updateCount(props.count);
1560
- });
1561
- }, [props.count]);
1562
- return <Text text={'Count: ' + count} />;
1563
- }
1564
- act(() => {
1565
- ReactNoop.render(<Counter count={0} />, () =>
1566
- Scheduler.unstable_yieldValue('Sync effect'),
1567
- );
1568
- expect(Scheduler).toFlushAndYieldThrough([
1569
- 'Count: (empty)',
1570
- 'Sync effect',
1571
- ]);
1572
- expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1573
- expect(() => {
1574
- ReactNoop.flushPassiveEffects();
1575
- }).toThrow('flushSync was called from inside a lifecycle method');
1576
- });
1389
+ // A discrete event forces the passive effect to be flushed --
1390
+ // updateCount(1) happens first, so 2 wins.
1391
+ ReactNoop.flushDiscreteUpdates();
1392
+ ReactNoop.discreteUpdates(() => {
1393
+ // (use batchedUpdates to silence the act() warning)
1394
+ ReactNoop.batchedUpdates(() => {
1395
+ _updateCount(2);
1396
});
1397
+ });
1398
+ expect(Scheduler).toHaveYielded(['Will set count to 1']);
1399
+ expect(() => {
1400
+ expect(Scheduler).toFlushAndYield(['Count: 2']);
1401
+ }).toErrorDev([
1402
+ 'An update to Counter ran an effect',
1403
+ 'An update to Counter ran an effect',
1404
+ ]);
1405
+
1406
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
1407
+ });
1408
1579
- it('unmounts previous effect', () => {
1580
- function Counter(props) {
1581
- useEffect(() => {
1582
- Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1583
- return () => {
1584
- Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1585
- };
1586
- });
1587
- return <Text text={'Count: ' + props.count} />;
1588
- }
1589
- act(() => {
1590
- ReactNoop.render(<Counter count={0} />, () =>
1591
- Scheduler.unstable_yieldValue('Sync effect'),
1592
- );
1593
- expect(Scheduler).toFlushAndYieldThrough([
1594
- 'Count: 0',
1595
- 'Sync effect',
1596
- ]);
1597
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1598
- });
1599
-
1600
- expect(Scheduler).toHaveYielded(['Did create [0]']);
1601
-
1602
- act(() => {
1603
- ReactNoop.render(<Counter count={1} />, () =>
1604
- Scheduler.unstable_yieldValue('Sync effect'),
1605
- );
1606
- expect(Scheduler).toFlushAndYieldThrough([
1607
- 'Count: 1',
1608
- 'Sync effect',
1609
- ]);
1610
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1611
- });
1409
+ it('flushes passive effects when flushing discrete updates (with tracing)', () => {
1410
+ const onInteractionScheduledWorkCompleted = jest.fn();
1411
+ const onWorkCanceled = jest.fn();
1412
+ SchedulerTracing.unstable_subscribe({
1413
+ onInteractionScheduledWorkCompleted,
1414
+ onInteractionTraced: jest.fn(),
1415
+ onWorkCanceled,
1416
+ onWorkScheduled: jest.fn(),
1417
+ onWorkStarted: jest.fn(),
1418
+ onWorkStopped: jest.fn(),
1419
+ });
1420
1613
- expect(Scheduler).toHaveYielded([
1614
- 'Did destroy [0]',
1615
- 'Did create [1]',
1421
+ let _updateCount;
1422
+ function Counter(props) {
1423
+ const [count, updateCount] = useState(0);
1424
+ _updateCount = updateCount;
1425
+ useEffect(() => {
1426
+ expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([
1427
+ tracingEvent,
1428
]);
1617
- });
1429
+ Scheduler.unstable_yieldValue(`Will set count to 1`);
1430
+ updateCount(1);
1431
+ }, []);
1432
+ return <Text text={'Count: ' + count} />;
1433
+ }
1434
1619
- it('unmounts on deletion', () => {
1620
- function Counter(props) {
1621
- useEffect(() => {
1622
- Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1623
- return () => {
1624
- Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1625
- };
1626
- });
1627
- return <Text text={'Count: ' + props.count} />;
1628
- }
1629
- act(() => {
1435
+ const tracingEvent = {id: 0, name: 'hello', timestamp: 0};
1436
+ // we explicitly wait for missing act() warnings here since
1437
+ // it's a lot harder to simulate this condition inside an act scope
1438
+ expect(() => {
1439
+ SchedulerTracing.unstable_trace(
1440
+ tracingEvent.name,
1441
+ tracingEvent.timestamp,
1442
+ () => {
1443
ReactNoop.render(<Counter count={0} />, () =>
1444
Scheduler.unstable_yieldValue('Sync effect'),
1445
);
1633
- expect(Scheduler).toFlushAndYieldThrough([
1634
- 'Count: 0',
1635
- 'Sync effect',
1636
- ]);
1637
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1638
- });
1446
+ },
1447
+ );
1448
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1449
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1450
+ }).toErrorDev(['An update to Counter ran an effect']);
1451
1640
- expect(Scheduler).toHaveYielded(['Did create [0]']);
1452
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0);
1453
1642
- ReactNoop.render(null);
1643
- expect(Scheduler).toFlushAndYield(['Did destroy [0]']);
1644
- expect(ReactNoop.getChildren()).toEqual([]);
1454
+ // A discrete event forces the passive effect to be flushed --
1455
+ // updateCount(1) happens first, so 2 wins.
1456
+ ReactNoop.flushDiscreteUpdates();
1457
+ ReactNoop.discreteUpdates(() => {
1458
+ // (use batchedUpdates to silence the act() warning)
1459
+ ReactNoop.batchedUpdates(() => {
1460
+ _updateCount(2);
1461
});
1462
+ });
1463
+ expect(Scheduler).toHaveYielded(['Will set count to 1']);
1464
+ expect(() => {
1465
+ expect(Scheduler).toFlushAndYield(['Count: 2']);
1466
+ }).toErrorDev([
1467
+ 'An update to Counter ran an effect',
1468
+ 'An update to Counter ran an effect',
1469
+ ]);
1470
+
1471
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]);
1472
+
1473
+ expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
1474
+ expect(onWorkCanceled).toHaveBeenCalledTimes(0);
1475
+ });
1476
1647
- it('unmounts on deletion after skipped effect', () => {
1648
- function Counter(props) {
1649
- useEffect(() => {
1650
- Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1651
- return () => {
1652
- Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1653
- };
1654
- }, []);
1655
- return <Text text={'Count: ' + props.count} />;
1656
- }
1657
- act(() => {
1658
- ReactNoop.render(<Counter count={0} />, () =>
1659
- Scheduler.unstable_yieldValue('Sync effect'),
1660
- );
1661
- expect(Scheduler).toFlushAndYieldThrough([
1662
- 'Count: 0',
1663
- 'Sync effect',
1664
- ]);
1665
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1666
- });
1667
-
1668
- expect(Scheduler).toHaveYielded(['Did create [0]']);
1477
+ it(
1478
+ 'in legacy mode, useEffect is deferred and updates finish synchronously ' +
1479
+ '(in a single batch)',
1480
+ () => {
1481
+ function Counter(props) {
1482
+ const [count, updateCount] = useState('(empty)');
1483
+ useEffect(() => {
1484
+ // Update multiple times. These should all be batched together in
1485
+ // a single render.
1486
+ updateCount(props.count);
1487
+ updateCount(props.count);
1488
+ updateCount(props.count);
1489
+ updateCount(props.count);
1490
+ updateCount(props.count);
1491
+ updateCount(props.count);
1492
+ }, [props.count]);
1493
+ return <Text text={'Count: ' + count} />;
1494
+ }
1495
+ act(() => {
1496
+ ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
1497
+ // Even in legacy mode, effects are deferred until after paint
1498
+ expect(Scheduler).toFlushAndYieldThrough(['Count: (empty)']);
1499
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1500
+ });
1501
1670
- act(() => {
1671
- ReactNoop.render(<Counter count={1} />, () =>
1672
- Scheduler.unstable_yieldValue('Sync effect'),
1673
- );
1674
- expect(Scheduler).toFlushAndYieldThrough([
1675
- 'Count: 1',
1676
- 'Sync effect',
1677
- ]);
1678
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1502
+ // effects get fored on exiting act()
1503
+ // There were multiple updates, but there should only be a
1504
+ // single render
1505
+ expect(Scheduler).toHaveYielded(['Count: 0']);
1506
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1507
+ },
1508
+ );
1509
+
1510
+ it('flushSync is not allowed', () => {
1511
+ function Counter(props) {
1512
+ const [count, updateCount] = useState('(empty)');
1513
+ useEffect(() => {
1514
+ Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`);
1515
+ ReactNoop.flushSync(() => {
1516
+ updateCount(props.count);
1517
});
1518
+ }, [props.count]);
1519
+ return <Text text={'Count: ' + count} />;
1520
+ }
1521
+ act(() => {
1522
+ ReactNoop.render(<Counter count={0} />, () =>
1523
+ Scheduler.unstable_yieldValue('Sync effect'),
1524
+ );
1525
+ expect(Scheduler).toFlushAndYieldThrough([
1526
+ 'Count: (empty)',
1527
+ 'Sync effect',
1528
+ ]);
1529
+ expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1530
+ expect(() => {
1531
+ ReactNoop.flushPassiveEffects();
1532
+ }).toThrow('flushSync was called from inside a lifecycle method');
1533
+ });
1534
+ });
1535
1681
- expect(Scheduler).toHaveYielded([]);
1682
-
1683
- ReactNoop.render(null);
1684
- expect(Scheduler).toFlushAndYield(['Did destroy [0]']);
1685
- expect(ReactNoop.getChildren()).toEqual([]);
1536
+ it('unmounts previous effect', () => {
1537
+ function Counter(props) {
1538
+ useEffect(() => {
1539
+ Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1540
+ return () => {
1541
+ Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1542
+ };
1543
});
1544
+ return <Text text={'Count: ' + props.count} />;
1545
+ }
1546
+ act(() => {
1547
+ ReactNoop.render(<Counter count={0} />, () =>
1548
+ Scheduler.unstable_yieldValue('Sync effect'),
1549
+ );
1550
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1551
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1552
+ });
1553
1688
- it('always fires effects if no dependencies are provided', () => {
1689
- function effect() {
1690
- Scheduler.unstable_yieldValue(`Did create`);
1691
- return () => {
1692
- Scheduler.unstable_yieldValue(`Did destroy`);
1693
- };
1694
- }
1695
- function Counter(props) {
1696
- useEffect(effect);
1697
- return <Text text={'Count: ' + props.count} />;
1698
- }
1699
- act(() => {
1700
- ReactNoop.render(<Counter count={0} />, () =>
1701
- Scheduler.unstable_yieldValue('Sync effect'),
1702
- );
1703
- expect(Scheduler).toFlushAndYieldThrough([
1704
- 'Count: 0',
1705
- 'Sync effect',
1706
- ]);
1707
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1708
- });
1709
-
1710
- expect(Scheduler).toHaveYielded(['Did create']);
1554
+ expect(Scheduler).toHaveYielded(['Did create [0]']);
1555
1712
- act(() => {
1713
- ReactNoop.render(<Counter count={1} />, () =>
1714
- Scheduler.unstable_yieldValue('Sync effect'),
1715
- );
1716
- expect(Scheduler).toFlushAndYieldThrough([
1717
- 'Count: 1',
1718
- 'Sync effect',
1719
- ]);
1720
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1721
- });
1556
+ act(() => {
1557
+ ReactNoop.render(<Counter count={1} />, () =>
1558
+ Scheduler.unstable_yieldValue('Sync effect'),
1559
+ );
1560
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1561
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1562
+ });
1563
1723
- expect(Scheduler).toHaveYielded(['Did destroy', 'Did create']);
1564
+ expect(Scheduler).toHaveYielded(['Did destroy [0]', 'Did create [1]']);
1565
+ });
1566
1725
- ReactNoop.render(null);
1726
- expect(Scheduler).toFlushAndYield(['Did destroy']);
1727
- expect(ReactNoop.getChildren()).toEqual([]);
1567
+ it('unmounts on deletion', () => {
1568
+ function Counter(props) {
1569
+ useEffect(() => {
1570
+ Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1571
+ return () => {
1572
+ Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1573
+ };
1574
});
1575
+ return <Text text={'Count: ' + props.count} />;
1576
+ }
1577
+ act(() => {
1578
+ ReactNoop.render(<Counter count={0} />, () =>
1579
+ Scheduler.unstable_yieldValue('Sync effect'),
1580
+ );
1581
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1582
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1583
+ });
1584
1730
- it('skips effect if inputs have not changed', () => {
1731
- function Counter(props) {
1732
- const text = `${props.label}: ${props.count}`;
1733
- useEffect(() => {
1734
- Scheduler.unstable_yieldValue(`Did create [${text}]`);
1735
- return () => {
1736
- Scheduler.unstable_yieldValue(`Did destroy [${text}]`);
1737
- };
1738
- }, [props.label, props.count]);
1739
- return <Text text={text} />;
1740
- }
1741
- act(() => {
1742
- ReactNoop.render(<Counter label="Count" count={0} />, () =>
1743
- Scheduler.unstable_yieldValue('Sync effect'),
1744
- );
1745
- expect(Scheduler).toFlushAndYieldThrough([
1746
- 'Count: 0',
1747
- 'Sync effect',
1748
- ]);
1749
- });
1585
+ expect(Scheduler).toHaveYielded(['Did create [0]']);
1586
1751
- expect(Scheduler).toHaveYielded(['Did create [Count: 0]']);
1752
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1587
+ ReactNoop.render(null);
1588
+ expect(Scheduler).toFlushAndYield(['Did destroy [0]']);
1589
+ expect(ReactNoop.getChildren()).toEqual([]);
1590
+ });
1591
1754
- act(() => {
1755
- ReactNoop.render(<Counter label="Count" count={1} />, () =>
1756
- Scheduler.unstable_yieldValue('Sync effect'),
1757
- );
1758
- // Count changed
1759
- expect(Scheduler).toFlushAndYieldThrough([
1760
- 'Count: 1',
1761
- 'Sync effect',
1762
- ]);
1763
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1764
- });
1592
+ it('unmounts on deletion after skipped effect', () => {
1593
+ function Counter(props) {
1594
+ useEffect(() => {
1595
+ Scheduler.unstable_yieldValue(`Did create [${props.count}]`);
1596
+ return () => {
1597
+ Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`);
1598
+ };
1599
+ }, []);
1600
+ return <Text text={'Count: ' + props.count} />;
1601
+ }
1602
+ act(() => {
1603
+ ReactNoop.render(<Counter count={0} />, () =>
1604
+ Scheduler.unstable_yieldValue('Sync effect'),
1605
+ );
1606
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1607
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1608
+ });
1609
1766
- expect(Scheduler).toHaveYielded([
1767
- 'Did destroy [Count: 0]',
1768
- 'Did create [Count: 1]',
1769
- ]);
1610
+ expect(Scheduler).toHaveYielded(['Did create [0]']);
1611
1771
- act(() => {
1772
- ReactNoop.render(<Counter label="Count" count={1} />, () =>
1773
- Scheduler.unstable_yieldValue('Sync effect'),
1774
- );
1775
- // Nothing changed, so no effect should have fired
1776
- expect(Scheduler).toFlushAndYieldThrough([
1777
- 'Count: 1',
1778
- 'Sync effect',
1779
- ]);
1780
- });
1612
+ act(() => {
1613
+ ReactNoop.render(<Counter count={1} />, () =>
1614
+ Scheduler.unstable_yieldValue('Sync effect'),
1615
+ );
1616
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1617
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1618
+ });
1619
1782
- expect(Scheduler).toHaveYielded([]);
1783
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1620
+ expect(Scheduler).toHaveYielded([]);
1621
1785
- act(() => {
1786
- ReactNoop.render(<Counter label="Total" count={1} />, () =>
1787
- Scheduler.unstable_yieldValue('Sync effect'),
1788
- );
1789
- // Label changed
1790
- expect(Scheduler).toFlushAndYieldThrough([
1791
- 'Total: 1',
1792
- 'Sync effect',
1793
- ]);
1794
- expect(ReactNoop.getChildren()).toEqual([span('Total: 1')]);
1795
- });
1622
+ ReactNoop.render(null);
1623
+ expect(Scheduler).toFlushAndYield(['Did destroy [0]']);
1624
+ expect(ReactNoop.getChildren()).toEqual([]);
1625
+ });
1626
1797
- expect(Scheduler).toHaveYielded([
1798
- 'Did destroy [Count: 1]',
1799
- 'Did create [Total: 1]',
1800
- ]);
1801
- });
1627
+ it('always fires effects if no dependencies are provided', () => {
1628
+ function effect() {
1629
+ Scheduler.unstable_yieldValue(`Did create`);
1630
+ return () => {
1631
+ Scheduler.unstable_yieldValue(`Did destroy`);
1632
+ };
1633
+ }
1634
+ function Counter(props) {
1635
+ useEffect(effect);
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(['Count: 0', 'Sync effect']);
1643
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1644
+ });
1645
1803
- it('multiple effects', () => {
1804
- function Counter(props) {
1805
- useEffect(() => {
1806
- Scheduler.unstable_yieldValue(`Did commit 1 [${props.count}]`);
1807
- });
1808
- useEffect(() => {
1809
- Scheduler.unstable_yieldValue(`Did commit 2 [${props.count}]`);
1810
- });
1811
- return <Text text={'Count: ' + props.count} />;
1812
- }
1813
- act(() => {
1814
- ReactNoop.render(<Counter count={0} />, () =>
1815
- Scheduler.unstable_yieldValue('Sync effect'),
1816
- );
1817
- expect(Scheduler).toFlushAndYieldThrough([
1818
- 'Count: 0',
1819
- 'Sync effect',
1820
- ]);
1821
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1822
- });
1646
+ expect(Scheduler).toHaveYielded(['Did create']);
1647
1824
- expect(Scheduler).toHaveYielded([
1825
- 'Did commit 1 [0]',
1826
- 'Did commit 2 [0]',
1827
- ]);
1648
+ act(() => {
1649
+ ReactNoop.render(<Counter count={1} />, () =>
1650
+ Scheduler.unstable_yieldValue('Sync effect'),
1651
+ );
1652
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1653
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1654
+ });
1655
1829
- act(() => {
1830
- ReactNoop.render(<Counter count={1} />, () =>
1831
- Scheduler.unstable_yieldValue('Sync effect'),
1832
- );
1833
- expect(Scheduler).toFlushAndYieldThrough([
1834
- 'Count: 1',
1835
- 'Sync effect',
1836
- ]);
1837
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1838
- });
1839
- expect(Scheduler).toHaveYielded([
1840
- 'Did commit 1 [1]',
1841
- 'Did commit 2 [1]',
1842
- ]);
1843
- });
1656
+ expect(Scheduler).toHaveYielded(['Did destroy', 'Did create']);
1657
1845
- it('unmounts all previous effects before creating any new ones', () => {
1846
- function Counter(props) {
1847
- useEffect(() => {
1848
- Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1849
- return () => {
1850
- Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1851
- };
1852
- });
1853
- useEffect(() => {
1854
- Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1855
- return () => {
1856
- Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1857
- };
1858
- });
1859
- return <Text text={'Count: ' + props.count} />;
1860
- }
1861
- act(() => {
1862
- ReactNoop.render(<Counter count={0} />, () =>
1863
- Scheduler.unstable_yieldValue('Sync effect'),
1864
- );
1865
- expect(Scheduler).toFlushAndYieldThrough([
1866
- 'Count: 0',
1867
- 'Sync effect',
1868
- ]);
1869
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1870
- });
1658
+ ReactNoop.render(null);
1659
+ expect(Scheduler).toFlushAndYield(['Did destroy']);
1660
+ expect(ReactNoop.getChildren()).toEqual([]);
1661
+ });
1662
1872
- expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1663
+ it('skips effect if inputs have not changed', () => {
1664
+ function Counter(props) {
1665
+ const text = `${props.label}: ${props.count}`;
1666
+ useEffect(() => {
1667
+ Scheduler.unstable_yieldValue(`Did create [${text}]`);
1668
+ return () => {
1669
+ Scheduler.unstable_yieldValue(`Did destroy [${text}]`);
1670
+ };
1671
+ }, [props.label, props.count]);
1672
+ return <Text text={text} />;
1673
+ }
1674
+ act(() => {
1675
+ ReactNoop.render(<Counter label="Count" count={0} />, () =>
1676
+ Scheduler.unstable_yieldValue('Sync effect'),
1677
+ );
1678
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1679
+ });
1680
1874
- act(() => {
1875
- ReactNoop.render(<Counter count={1} />, () =>
1876
- Scheduler.unstable_yieldValue('Sync effect'),
1877
- );
1878
- expect(Scheduler).toFlushAndYieldThrough([
1879
- 'Count: 1',
1880
- 'Sync effect',
1881
- ]);
1882
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1883
- });
1884
- expect(Scheduler).toHaveYielded([
1885
- 'Unmount A [0]',
1886
- 'Unmount B [0]',
1887
- 'Mount A [1]',
1888
- 'Mount B [1]',
1889
- ]);
1890
- });
1681
+ expect(Scheduler).toHaveYielded(['Did create [Count: 0]']);
1682
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1683
1892
- if (runAllPassiveEffectDestroysBeforeCreates) {
1893
- it('unmounts all previous effects between siblings before creating any new ones', () => {
1894
- function Counter({count, label}) {
1895
- useEffect(() => {
1896
- Scheduler.unstable_yieldValue(`Mount ${label} [${count}]`);
1897
- return () => {
1898
- Scheduler.unstable_yieldValue(`Unmount ${label} [${count}]`);
1899
- };
1900
- });
1901
- return <Text text={`${label} ${count}`} />;
1902
- }
1903
- act(() => {
1904
- ReactNoop.render(
1905
- <>
1906
- <Counter label="A" count={0} />
1907
- <Counter label="B" count={0} />
1908
- </>,
1909
- () => Scheduler.unstable_yieldValue('Sync effect'),
1910
- );
1911
- expect(Scheduler).toFlushAndYieldThrough([
1912
- 'A 0',
1913
- 'B 0',
1914
- 'Sync effect',
1915
- ]);
1916
- expect(ReactNoop.getChildren()).toEqual([
1917
- span('A 0'),
1918
- span('B 0'),
1919
- ]);
1920
- });
1684
+ act(() => {
1685
+ ReactNoop.render(<Counter label="Count" count={1} />, () =>
1686
+ Scheduler.unstable_yieldValue('Sync effect'),
1687
+ );
1688
+ // Count changed
1689
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1690
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1691
+ });
1692
1922
- expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1923
-
1924
- act(() => {
1925
- ReactNoop.render(
1926
- <>
1927
- <Counter label="A" count={1} />
1928
- <Counter label="B" count={1} />
1929
- </>,
1930
- () => Scheduler.unstable_yieldValue('Sync effect'),
1931
- );
1932
- expect(Scheduler).toFlushAndYieldThrough([
1933
- 'A 1',
1934
- 'B 1',
1935
- 'Sync effect',
1936
- ]);
1937
- expect(ReactNoop.getChildren()).toEqual([
1938
- span('A 1'),
1939
- span('B 1'),
1940
- ]);
1941
- });
1942
- expect(Scheduler).toHaveYielded([
1943
- 'Unmount A [0]',
1944
- 'Unmount B [0]',
1945
- 'Mount A [1]',
1946
- 'Mount B [1]',
1947
- ]);
1948
-
1949
- act(() => {
1950
- ReactNoop.render(
1951
- <>
1952
- <Counter label="B" count={2} />
1953
- <Counter label="C" count={0} />
1954
- </>,
1955
- () => Scheduler.unstable_yieldValue('Sync effect'),
1956
- );
1957
- expect(Scheduler).toFlushAndYieldThrough([
1958
- 'B 2',
1959
- 'C 0',
1960
- 'Sync effect',
1961
- ]);
1962
- expect(ReactNoop.getChildren()).toEqual([
1963
- span('B 2'),
1964
- span('C 0'),
1965
- ]);
1966
- });
1967
- expect(Scheduler).toHaveYielded([
1968
- 'Unmount A [1]',
1969
- 'Unmount B [1]',
1970
- 'Mount B [2]',
1971
- 'Mount C [0]',
1972
- ]);
1973
- });
1974
- }
1693
+ expect(Scheduler).toHaveYielded([
1694
+ 'Did destroy [Count: 0]',
1695
+ 'Did create [Count: 1]',
1696
+ ]);
1697
1976
- it('handles errors in create on mount', () => {
1977
- function Counter(props) {
1978
- useEffect(() => {
1979
- Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1980
- return () => {
1981
- Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1982
- };
1983
- });
1984
- useEffect(() => {
1985
- Scheduler.unstable_yieldValue('Oops!');
1986
- throw new Error('Oops!');
1987
- // eslint-disable-next-line no-unreachable
1988
- Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1989
- return () => {
1990
- Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1991
- };
1992
- });
1993
- return <Text text={'Count: ' + props.count} />;
1994
- }
1995
- act(() => {
1996
- ReactNoop.render(<Counter count={0} />, () =>
1997
- Scheduler.unstable_yieldValue('Sync effect'),
1998
- );
1999
- expect(Scheduler).toFlushAndYieldThrough([
2000
- 'Count: 0',
2001
- 'Sync effect',
2002
- ]);
2003
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2004
- expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
2005
- });
1698
+ act(() => {
1699
+ ReactNoop.render(<Counter label="Count" count={1} />, () =>
1700
+ Scheduler.unstable_yieldValue('Sync effect'),
1701
+ );
1702
+ // Nothing changed, so no effect should have fired
1703
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1704
+ });
1705
2007
- expect(Scheduler).toHaveYielded([
2008
- 'Mount A [0]',
2009
- 'Oops!',
2010
- // Clean up effect A. There's no effect B to clean-up, because it
2011
- // never mounted.
2012
- 'Unmount A [0]',
2013
- ]);
2014
- expect(ReactNoop.getChildren()).toEqual([]);
2015
- });
1706
+ expect(Scheduler).toHaveYielded([]);
1707
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1708
2017
- it('handles errors in create on update', () => {
2018
- function Counter(props) {
2019
- useEffect(() => {
2020
- Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
2021
- return () => {
2022
- Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
2023
- };
2024
- });
2025
- useEffect(() => {
2026
- if (props.count === 1) {
2027
- Scheduler.unstable_yieldValue('Oops!');
2028
- throw new Error('Oops!');
2029
- }
2030
- Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
2031
- return () => {
2032
- Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
2033
- };
2034
- });
2035
- return <Text text={'Count: ' + props.count} />;
2036
- }
2037
- act(() => {
2038
- ReactNoop.render(<Counter count={0} />, () =>
2039
- Scheduler.unstable_yieldValue('Sync effect'),
2040
- );
2041
- expect(Scheduler).toFlushAndYieldThrough([
2042
- 'Count: 0',
2043
- 'Sync effect',
2044
- ]);
2045
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2046
- ReactNoop.flushPassiveEffects();
2047
- expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
2048
- });
1709
+ act(() => {
1710
+ ReactNoop.render(<Counter label="Total" count={1} />, () =>
1711
+ Scheduler.unstable_yieldValue('Sync effect'),
1712
+ );
1713
+ // Label changed
1714
+ expect(Scheduler).toFlushAndYieldThrough(['Total: 1', 'Sync effect']);
1715
+ expect(ReactNoop.getChildren()).toEqual([span('Total: 1')]);
1716
+ });
1717
2050
- act(() => {
2051
- // This update will trigger an error
2052
- ReactNoop.render(<Counter count={1} />, () =>
2053
- Scheduler.unstable_yieldValue('Sync effect'),
2054
- );
2055
- expect(Scheduler).toFlushAndYieldThrough([
2056
- 'Count: 1',
2057
- 'Sync effect',
2058
- ]);
2059
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2060
- expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
2061
- expect(Scheduler).toHaveYielded(
2062
- deferPassiveEffectCleanupDuringUnmount &&
2063
- runAllPassiveEffectDestroysBeforeCreates
2064
- ? ['Unmount A [0]', 'Unmount B [0]', 'Mount A [1]', 'Oops!']
2065
- : [
2066
- 'Unmount A [0]',
2067
- 'Unmount B [0]',
2068
- 'Mount A [1]',
2069
- 'Oops!',
2070
- 'Unmount A [1]',
2071
- ],
2072
- );
2073
- expect(ReactNoop.getChildren()).toEqual([]);
2074
- });
2075
- if (
2076
- deferPassiveEffectCleanupDuringUnmount &&
2077
- runAllPassiveEffectDestroysBeforeCreates
2078
- ) {
2079
- expect(Scheduler).toHaveYielded([
2080
- // Clean up effect A runs passively on unmount.
2081
- // There's no effect B to clean-up, because it never mounted.
2082
- 'Unmount A [1]',
2083
- ]);
2084
- }
2085
- });
1718
+ expect(Scheduler).toHaveYielded([
1719
+ 'Did destroy [Count: 1]',
1720
+ 'Did create [Total: 1]',
1721
+ ]);
1722
+ });
1723
2087
- it('handles errors in destroy on update', () => {
2088
- function Counter(props) {
2089
- useEffect(() => {
2090
- Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
2091
- return () => {
2092
- Scheduler.unstable_yieldValue('Oops!');
2093
- if (props.count === 0) {
2094
- throw new Error('Oops!');
2095
- }
2096
- };
2097
- });
2098
- useEffect(() => {
2099
- Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
2100
- return () => {
2101
- Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
2102
- };
2103
- });
2104
- return <Text text={'Count: ' + props.count} />;
2105
- }
1724
+ it('multiple effects', () => {
1725
+ function Counter(props) {
1726
+ useEffect(() => {
1727
+ Scheduler.unstable_yieldValue(`Did commit 1 [${props.count}]`);
1728
+ });
1729
+ useEffect(() => {
1730
+ Scheduler.unstable_yieldValue(`Did commit 2 [${props.count}]`);
1731
+ });
1732
+ return <Text text={'Count: ' + props.count} />;
1733
+ }
1734
+ act(() => {
1735
+ ReactNoop.render(<Counter count={0} />, () =>
1736
+ Scheduler.unstable_yieldValue('Sync effect'),
1737
+ );
1738
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1739
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1740
+ });
1741
2107
- act(() => {
2108
- ReactNoop.render(<Counter count={0} />, () =>
2109
- Scheduler.unstable_yieldValue('Sync effect'),
2110
- );
2111
- expect(Scheduler).toFlushAndYieldThrough([
2112
- 'Count: 0',
2113
- 'Sync effect',
2114
- ]);
2115
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2116
- ReactNoop.flushPassiveEffects();
2117
- expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
2118
- });
1742
+ expect(Scheduler).toHaveYielded(['Did commit 1 [0]', 'Did commit 2 [0]']);
1743
2120
- if (
2121
- deferPassiveEffectCleanupDuringUnmount &&
2122
- runAllPassiveEffectDestroysBeforeCreates
2123
- ) {
2124
- act(() => {
2125
- // This update will trigger an error during passive effect unmount
2126
- ReactNoop.render(<Counter count={1} />, () =>
2127
- Scheduler.unstable_yieldValue('Sync effect'),
2128
- );
2129
- expect(Scheduler).toFlushAndYieldThrough([
2130
- 'Count: 1',
2131
- 'Sync effect',
2132
- ]);
2133
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2134
- expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
2135
-
2136
- // This branch enables a feature flag that flushes all passive destroys in a
2137
- // separate pass before flushing any passive creates.
2138
- // A result of this two-pass flush is that an error thrown from unmount does
2139
- // not block the subsequent create functions from being run.
2140
- expect(Scheduler).toHaveYielded([
2141
- 'Oops!',
2142
- 'Unmount B [0]',
2143
- 'Mount A [1]',
2144
- 'Mount B [1]',
2145
- ]);
2146
- });
1744
+ act(() => {
1745
+ ReactNoop.render(<Counter count={1} />, () =>
1746
+ Scheduler.unstable_yieldValue('Sync effect'),
1747
+ );
1748
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1749
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1750
+ });
1751
+ expect(Scheduler).toHaveYielded(['Did commit 1 [1]', 'Did commit 2 [1]']);
1752
+ });
1753
2148
- // <Counter> gets unmounted because an error is thrown above.
2149
- // The remaining destroy functions are run later on unmount, since they're passive.
2150
- // In this case, one of them throws again (because of how the test is written).
2151
- expect(Scheduler).toHaveYielded(['Oops!', 'Unmount B [1]']);
2152
- expect(ReactNoop.getChildren()).toEqual([]);
2153
- } else {
2154
- act(() => {
2155
- // This update will trigger an error during passive effect unmount
2156
- ReactNoop.render(<Counter count={1} />, () =>
2157
- Scheduler.unstable_yieldValue('Sync effect'),
2158
- );
2159
- expect(() => {
2160
- expect(Scheduler).toFlushAndYield(['Count: 1', 'Sync effect']);
2161
- }).toThrow('Oops!');
2162
- expect(ReactNoop.getChildren()).toEqual([]);
2163
- ReactNoop.flushPassiveEffects();
2164
- });
2165
- }
1754
+ it('unmounts all previous effects before creating any new ones', () => {
1755
+ function Counter(props) {
1756
+ useEffect(() => {
1757
+ Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1758
+ return () => {
1759
+ Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1760
+ };
1761
+ });
1762
+ useEffect(() => {
1763
+ Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1764
+ return () => {
1765
+ Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1766
+ };
1767
});
1768
+ return <Text text={'Count: ' + props.count} />;
1769
+ }
1770
+ act(() => {
1771
+ ReactNoop.render(<Counter count={0} />, () =>
1772
+ Scheduler.unstable_yieldValue('Sync effect'),
1773
+ );
1774
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1775
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1776
+ });
1777
2168
- it('works with memo', () => {
2169
- function Counter({count}) {
2170
- useLayoutEffect(() => {
2171
- Scheduler.unstable_yieldValue('Mount: ' + count);
2172
- return () => Scheduler.unstable_yieldValue('Unmount: ' + count);
2173
- });
2174
- return <Text text={'Count: ' + count} />;
2175
- }
2176
- Counter = memo(Counter);
1778
+ expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1779
2178
- ReactNoop.render(<Counter count={0} />, () =>
2179
- Scheduler.unstable_yieldValue('Sync effect'),
2180
- );
2181
- expect(Scheduler).toFlushAndYieldThrough([
2182
- 'Count: 0',
2183
- 'Mount: 0',
2184
- 'Sync effect',
2185
- ]);
2186
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1780
+ act(() => {
1781
+ ReactNoop.render(<Counter count={1} />, () =>
1782
+ Scheduler.unstable_yieldValue('Sync effect'),
1783
+ );
1784
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1785
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1786
+ });
1787
+ expect(Scheduler).toHaveYielded([
1788
+ 'Unmount A [0]',
1789
+ 'Unmount B [0]',
1790
+ 'Mount A [1]',
1791
+ 'Mount B [1]',
1792
+ ]);
1793
+ });
1794
2188
- ReactNoop.render(<Counter count={1} />, () =>
2189
- Scheduler.unstable_yieldValue('Sync effect'),
1795
+ if (runAllPassiveEffectDestroysBeforeCreates) {
1796
+ it('unmounts all previous effects between siblings before creating any new ones', () => {
1797
+ function Counter({count, label}) {
1798
+ useEffect(() => {
1799
+ Scheduler.unstable_yieldValue(`Mount ${label} [${count}]`);
1800
+ return () => {
1801
+ Scheduler.unstable_yieldValue(`Unmount ${label} [${count}]`);
1802
+ };
1803
+ });
1804
+ return <Text text={`${label} ${count}`} />;
1805
+ }
1806
+ act(() => {
1807
+ ReactNoop.render(
1808
+ <>
1809
+ <Counter label="A" count={0} />
1810
+ <Counter label="B" count={0} />
1811
+ </>,
1812
+ () => Scheduler.unstable_yieldValue('Sync effect'),
1813
);
1814
expect(Scheduler).toFlushAndYieldThrough([
2192
- 'Count: 1',
2193
- 'Unmount: 0',
2194
- 'Mount: 1',
1815
+ 'A 0',
1816
+ 'B 0',
1817
'Sync effect',
1818
]);
2197
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2198
-
2199
- ReactNoop.render(null);
2200
- expect(Scheduler).toFlushAndYieldThrough(['Unmount: 1']);
2201
- expect(ReactNoop.getChildren()).toEqual([]);
1819
+ expect(ReactNoop.getChildren()).toEqual([span('A 0'), span('B 0')]);
1820
});
2203
- });
1821
2205
- describe('useLayoutEffect', () => {
2206
- it('fires layout effects after the host has been mutated', () => {
2207
- function getCommittedText() {
2208
- const yields = Scheduler.unstable_clearYields();
2209
- const children = ReactNoop.getChildren();
2210
- Scheduler.unstable_yieldValue(yields);
2211
- if (children === null) {
2212
- return null;
2213
- }
2214
- return children[0].prop;
2215
- }
1822
+ expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1823
2217
- function Counter(props) {
2218
- useLayoutEffect(() => {
2219
- Scheduler.unstable_yieldValue(`Current: ${getCommittedText()}`);
2220
- });
2221
- return <Text text={props.count} />;
2222
- }
2223
-
2224
- ReactNoop.render(<Counter count={0} />, () =>
2225
- Scheduler.unstable_yieldValue('Sync effect'),
1824
+ act(() => {
1825
+ ReactNoop.render(
1826
+ <>
1827
+ <Counter label="A" count={1} />
1828
+ <Counter label="B" count={1} />
1829
+ </>,
1830
+ () => Scheduler.unstable_yieldValue('Sync effect'),
1831
);
1832
expect(Scheduler).toFlushAndYieldThrough([
2228
- [0],
2229
- 'Current: 0',
1833
+ 'A 1',
1834
+ 'B 1',
1835
'Sync effect',
1836
]);
2232
- expect(ReactNoop.getChildren()).toEqual([span(0)]);
1837
+ expect(ReactNoop.getChildren()).toEqual([span('A 1'), span('B 1')]);
1838
+ });
1839
+ expect(Scheduler).toHaveYielded([
1840
+ 'Unmount A [0]',
1841
+ 'Unmount B [0]',
1842
+ 'Mount A [1]',
1843
+ 'Mount B [1]',
1844
+ ]);
1845
2234
- ReactNoop.render(<Counter count={1} />, () =>
2235
- Scheduler.unstable_yieldValue('Sync effect'),
1846
+ act(() => {
1847
+ ReactNoop.render(
1848
+ <>
1849
+ <Counter label="B" count={2} />
1850
+ <Counter label="C" count={0} />
1851
+ </>,
1852
+ () => Scheduler.unstable_yieldValue('Sync effect'),
1853
);
1854
expect(Scheduler).toFlushAndYieldThrough([
2238
- [1],
2239
- 'Current: 1',
1855
+ 'B 2',
1856
+ 'C 0',
1857
'Sync effect',
1858
]);
2242
- expect(ReactNoop.getChildren()).toEqual([span(1)]);
1859
+ expect(ReactNoop.getChildren()).toEqual([span('B 2'), span('C 0')]);
1860
});
1861
+ expect(Scheduler).toHaveYielded([
1862
+ 'Unmount A [1]',
1863
+ 'Unmount B [1]',
1864
+ 'Mount B [2]',
1865
+ 'Mount C [0]',
1866
+ ]);
1867
+ });
1868
+ }
1869
+
1870
+ it('handles errors in create on mount', () => {
1871
+ function Counter(props) {
1872
+ useEffect(() => {
1873
+ Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1874
+ return () => {
1875
+ Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1876
+ };
1877
+ });
1878
+ useEffect(() => {
1879
+ Scheduler.unstable_yieldValue('Oops!');
1880
+ throw new Error('Oops!');
1881
+ // eslint-disable-next-line no-unreachable
1882
+ Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1883
+ return () => {
1884
+ Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1885
+ };
1886
+ });
1887
+ return <Text text={'Count: ' + props.count} />;
1888
+ }
1889
+ act(() => {
1890
+ ReactNoop.render(<Counter count={0} />, () =>
1891
+ Scheduler.unstable_yieldValue('Sync effect'),
1892
+ );
1893
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1894
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1895
+ expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1896
+ });
1897
2245
- it('force flushes passive effects before firing new layout effects', () => {
2246
- let committedText = '(empty)';
2247
-
2248
- function Counter(props) {
2249
- useLayoutEffect(() => {
2250
- // Normally this would go in a mutation effect, but this test
2251
- // intentionally omits a mutation effect.
2252
- committedText = props.count + '';
2253
-
2254
- Scheduler.unstable_yieldValue(
2255
- `Mount layout [current: ${committedText}]`,
2256
- );
2257
- return () => {
2258
- Scheduler.unstable_yieldValue(
2259
- `Unmount layout [current: ${committedText}]`,
2260
- );
2261
- };
2262
- });
2263
- useEffect(() => {
2264
- Scheduler.unstable_yieldValue(
2265
- `Mount normal [current: ${committedText}]`,
2266
- );
2267
- return () => {
2268
- Scheduler.unstable_yieldValue(
2269
- `Unmount normal [current: ${committedText}]`,
2270
- );
2271
- };
2272
- });
2273
- return null;
2274
- }
2275
-
2276
- act(() => {
2277
- ReactNoop.render(<Counter count={0} />, () =>
2278
- Scheduler.unstable_yieldValue('Sync effect'),
2279
- );
2280
- expect(Scheduler).toFlushAndYieldThrough([
2281
- 'Mount layout [current: 0]',
2282
- 'Sync effect',
2283
- ]);
2284
- expect(committedText).toEqual('0');
2285
- ReactNoop.render(<Counter count={1} />, () =>
2286
- Scheduler.unstable_yieldValue('Sync effect'),
2287
- );
2288
- expect(Scheduler).toFlushAndYieldThrough([
2289
- 'Mount normal [current: 0]',
2290
- 'Unmount layout [current: 0]',
2291
- 'Mount layout [current: 1]',
2292
- 'Sync effect',
2293
- ]);
2294
- expect(committedText).toEqual('1');
2295
- });
1898
+ expect(Scheduler).toHaveYielded([
1899
+ 'Mount A [0]',
1900
+ 'Oops!',
1901
+ // Clean up effect A. There's no effect B to clean-up, because it
1902
+ // never mounted.
1903
+ 'Unmount A [0]',
1904
+ ]);
1905
+ expect(ReactNoop.getChildren()).toEqual([]);
1906
+ });
1907
2297
- expect(Scheduler).toHaveYielded([
2298
- 'Unmount normal [current: 1]',
2299
- 'Mount normal [current: 1]',
2300
- ]);
1908
+ it('handles errors in create on update', () => {
1909
+ function Counter(props) {
1910
+ useEffect(() => {
1911
+ Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1912
+ return () => {
1913
+ Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`);
1914
+ };
1915
+ });
1916
+ useEffect(() => {
1917
+ if (props.count === 1) {
1918
+ Scheduler.unstable_yieldValue('Oops!');
1919
+ throw new Error('Oops!');
1920
+ }
1921
+ Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1922
+ return () => {
1923
+ Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1924
+ };
1925
});
1926
+ return <Text text={'Count: ' + props.count} />;
1927
+ }
1928
+ act(() => {
1929
+ ReactNoop.render(<Counter count={0} />, () =>
1930
+ Scheduler.unstable_yieldValue('Sync effect'),
1931
+ );
1932
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1933
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1934
+ ReactNoop.flushPassiveEffects();
1935
+ expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
1936
});
1937
2304
- describe('useCallback', () => {
2305
- it('memoizes callback by comparing inputs', () => {
2306
- class IncrementButton extends React.PureComponent {
2307
- increment = () => {
2308
- this.props.increment();
2309
- };
2310
- render() {
2311
- return <Text text="Increment" />;
1938
+ act(() => {
1939
+ // This update will trigger an error
1940
+ ReactNoop.render(<Counter count={1} />, () =>
1941
+ Scheduler.unstable_yieldValue('Sync effect'),
1942
+ );
1943
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
1944
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
1945
+ expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
1946
+ expect(Scheduler).toHaveYielded(
1947
+ deferPassiveEffectCleanupDuringUnmount &&
1948
+ runAllPassiveEffectDestroysBeforeCreates
1949
+ ? ['Unmount A [0]', 'Unmount B [0]', 'Mount A [1]', 'Oops!']
1950
+ : [
1951
+ 'Unmount A [0]',
1952
+ 'Unmount B [0]',
1953
+ 'Mount A [1]',
1954
+ 'Oops!',
1955
+ 'Unmount A [1]',
1956
+ ],
1957
+ );
1958
+ expect(ReactNoop.getChildren()).toEqual([]);
1959
+ });
1960
+ if (
1961
+ deferPassiveEffectCleanupDuringUnmount &&
1962
+ runAllPassiveEffectDestroysBeforeCreates
1963
+ ) {
1964
+ expect(Scheduler).toHaveYielded([
1965
+ // Clean up effect A runs passively on unmount.
1966
+ // There's no effect B to clean-up, because it never mounted.
1967
+ 'Unmount A [1]',
1968
+ ]);
1969
+ }
1970
+ });
1971
+
1972
+ it('handles errors in destroy on update', () => {
1973
+ function Counter(props) {
1974
+ useEffect(() => {
1975
+ Scheduler.unstable_yieldValue(`Mount A [${props.count}]`);
1976
+ return () => {
1977
+ Scheduler.unstable_yieldValue('Oops!');
1978
+ if (props.count === 0) {
1979
+ throw new Error('Oops!');
1980
}
2313
- }
1981
+ };
1982
+ });
1983
+ useEffect(() => {
1984
+ Scheduler.unstable_yieldValue(`Mount B [${props.count}]`);
1985
+ return () => {
1986
+ Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`);
1987
+ };
1988
+ });
1989
+ return <Text text={'Count: ' + props.count} />;
1990
+ }
1991
2315
- function Counter({incrementBy}) {
2316
- const [count, updateCount] = useState(0);
2317
- const increment = useCallback(
2318
- () => updateCount(c => c + incrementBy),
2319
- [incrementBy],
2320
- );
2321
- return (
2322
- <>
2323
- <IncrementButton increment={increment} ref={button} />
2324
- <Text text={'Count: ' + count} />
2325
- </>
2326
- );
2327
- }
1992
+ act(() => {
1993
+ ReactNoop.render(<Counter count={0} />, () =>
1994
+ Scheduler.unstable_yieldValue('Sync effect'),
1995
+ );
1996
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']);
1997
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
1998
+ ReactNoop.flushPassiveEffects();
1999
+ expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']);
2000
+ });
2001
2329
- const button = React.createRef(null);
2330
- ReactNoop.render(<Counter incrementBy={1} />);
2331
- expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']);
2332
- expect(ReactNoop.getChildren()).toEqual([
2333
- span('Increment'),
2334
- span('Count: 0'),
2335
- ]);
2002
+ if (
2003
+ deferPassiveEffectCleanupDuringUnmount &&
2004
+ runAllPassiveEffectDestroysBeforeCreates
2005
+ ) {
2006
+ act(() => {
2007
+ // This update will trigger an error during passive effect unmount
2008
+ ReactNoop.render(<Counter count={1} />, () =>
2009
+ Scheduler.unstable_yieldValue('Sync effect'),
2010
+ );
2011
+ expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']);
2012
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2013
+ expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');
2014
2337
- act(button.current.increment);
2015
+ // This branch enables a feature flag that flushes all passive destroys in a
2016
+ // separate pass before flushing any passive creates.
2017
+ // A result of this two-pass flush is that an error thrown from unmount does
2018
+ // not block the subsequent create functions from being run.
2019
expect(Scheduler).toHaveYielded([
2339
- // Button should not re-render, because its props haven't changed
2340
- // 'Increment',
2341
- 'Count: 1',
2342
- ]);
2343
- expect(ReactNoop.getChildren()).toEqual([
2344
- span('Increment'),
2345
- span('Count: 1'),
2020
+ 'Oops!',
2021
+ 'Unmount B [0]',
2022
+ 'Mount A [1]',
2023
+ 'Mount B [1]',
2024
]);
2025
+ });
2026
2348
- // Increase the increment amount
2349
- ReactNoop.render(<Counter incrementBy={10} />);
2350
- expect(Scheduler).toFlushAndYield([
2351
- // Inputs did change this time
2352
- 'Increment',
2353
- 'Count: 1',
2354
- ]);
2355
- expect(ReactNoop.getChildren()).toEqual([
2356
- span('Increment'),
2357
- span('Count: 1'),
2358
- ]);
2027
+ // <Counter> gets unmounted because an error is thrown above.
2028
+ // The remaining destroy functions are run later on unmount, since they're passive.
2029
+ // In this case, one of them throws again (because of how the test is written).
2030
+ expect(Scheduler).toHaveYielded(['Oops!', 'Unmount B [1]']);
2031
+ expect(ReactNoop.getChildren()).toEqual([]);
2032
+ } else {
2033
+ act(() => {
2034
+ // This update will trigger an error during passive effect unmount
2035
+ ReactNoop.render(<Counter count={1} />, () =>
2036
+ Scheduler.unstable_yieldValue('Sync effect'),
2037
+ );
2038
+ expect(() => {
2039
+ expect(Scheduler).toFlushAndYield(['Count: 1', 'Sync effect']);
2040
+ }).toThrow('Oops!');
2041
+ expect(ReactNoop.getChildren()).toEqual([]);
2042
+ ReactNoop.flushPassiveEffects();
2043
+ });
2044
+ }
2045
+ });
2046
+
2047
+ it('works with memo', () => {
2048
+ function Counter({count}) {
2049
+ useLayoutEffect(() => {
2050
+ Scheduler.unstable_yieldValue('Mount: ' + count);
2051
+ return () => Scheduler.unstable_yieldValue('Unmount: ' + count);
2052
+ });
2053
+ return <Text text={'Count: ' + count} />;
2054
+ }
2055
+ Counter = memo(Counter);
2056
+
2057
+ ReactNoop.render(<Counter count={0} />, () =>
2058
+ Scheduler.unstable_yieldValue('Sync effect'),
2059
+ );
2060
+ expect(Scheduler).toFlushAndYieldThrough([
2061
+ 'Count: 0',
2062
+ 'Mount: 0',
2063
+ 'Sync effect',
2064
+ ]);
2065
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2066
+
2067
+ ReactNoop.render(<Counter count={1} />, () =>
2068
+ Scheduler.unstable_yieldValue('Sync effect'),
2069
+ );
2070
+ expect(Scheduler).toFlushAndYieldThrough([
2071
+ 'Count: 1',
2072
+ 'Unmount: 0',
2073
+ 'Mount: 1',
2074
+ 'Sync effect',
2075
+ ]);
2076
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2077
+
2078
+ ReactNoop.render(null);
2079
+ expect(Scheduler).toFlushAndYieldThrough(['Unmount: 1']);
2080
+ expect(ReactNoop.getChildren()).toEqual([]);
2081
+ });
2082
+ });
2083
+
2084
+ describe('useLayoutEffect', () => {
2085
+ it('fires layout effects after the host has been mutated', () => {
2086
+ function getCommittedText() {
2087
+ const yields = Scheduler.unstable_clearYields();
2088
+ const children = ReactNoop.getChildren();
2089
+ Scheduler.unstable_yieldValue(yields);
2090
+ if (children === null) {
2091
+ return null;
2092
+ }
2093
+ return children[0].prop;
2094
+ }
2095
2360
- // Callback should have updated
2361
- act(button.current.increment);
2362
- expect(Scheduler).toHaveYielded(['Count: 11']);
2363
- expect(ReactNoop.getChildren()).toEqual([
2364
- span('Increment'),
2365
- span('Count: 11'),
2366
- ]);
2096
+ function Counter(props) {
2097
+ useLayoutEffect(() => {
2098
+ Scheduler.unstable_yieldValue(`Current: ${getCommittedText()}`);
2099
});
2368
- });
2369
-
2370
- describe('useMemo', () => {
2371
- it('memoizes value by comparing to previous inputs', () => {
2372
- function CapitalizedText(props) {
2373
- const text = props.text;
2374
- const capitalizedText = useMemo(() => {
2375
- Scheduler.unstable_yieldValue(`Capitalize '${text}'`);
2376
- return text.toUpperCase();
2377
- }, [text]);
2378
- return <Text text={capitalizedText} />;
2379
- }
2100
+ return <Text text={props.count} />;
2101
+ }
2102
2381
- ReactNoop.render(<CapitalizedText text="hello" />);
2382
- expect(Scheduler).toFlushAndYield(["Capitalize 'hello'", 'HELLO']);
2383
- expect(ReactNoop.getChildren()).toEqual([span('HELLO')]);
2103
+ ReactNoop.render(<Counter count={0} />, () =>
2104
+ Scheduler.unstable_yieldValue('Sync effect'),
2105
+ );
2106
+ expect(Scheduler).toFlushAndYieldThrough([
2107
+ [0],
2108
+ 'Current: 0',
2109
+ 'Sync effect',
2110
+ ]);
2111
+ expect(ReactNoop.getChildren()).toEqual([span(0)]);
2112
+
2113
+ ReactNoop.render(<Counter count={1} />, () =>
2114
+ Scheduler.unstable_yieldValue('Sync effect'),
2115
+ );
2116
+ expect(Scheduler).toFlushAndYieldThrough([
2117
+ [1],
2118
+ 'Current: 1',
2119
+ 'Sync effect',
2120
+ ]);
2121
+ expect(ReactNoop.getChildren()).toEqual([span(1)]);
2122
+ });
2123
2385
- ReactNoop.render(<CapitalizedText text="hi" />);
2386
- expect(Scheduler).toFlushAndYield(["Capitalize 'hi'", 'HI']);
2387
- expect(ReactNoop.getChildren()).toEqual([span('HI')]);
2124
+ it('force flushes passive effects before firing new layout effects', () => {
2125
+ let committedText = '(empty)';
2126
2389
- ReactNoop.render(<CapitalizedText text="hi" />);
2390
- expect(Scheduler).toFlushAndYield(['HI']);
2391
- expect(ReactNoop.getChildren()).toEqual([span('HI')]);
2127
+ function Counter(props) {
2128
+ useLayoutEffect(() => {
2129
+ // Normally this would go in a mutation effect, but this test
2130
+ // intentionally omits a mutation effect.
2131
+ committedText = props.count + '';
2132
2393
- ReactNoop.render(<CapitalizedText text="goodbye" />);
2394
- expect(Scheduler).toFlushAndYield([
2395
- "Capitalize 'goodbye'",
2396
- 'GOODBYE',
2397
- ]);
2398
- expect(ReactNoop.getChildren()).toEqual([span('GOODBYE')]);
2133
+ Scheduler.unstable_yieldValue(
2134
+ `Mount layout [current: ${committedText}]`,
2135
+ );
2136
+ return () => {
2137
+ Scheduler.unstable_yieldValue(
2138
+ `Unmount layout [current: ${committedText}]`,
2139
+ );
2140
+ };
2141
+ });
2142
+ useEffect(() => {
2143
+ Scheduler.unstable_yieldValue(
2144
+ `Mount normal [current: ${committedText}]`,
2145
+ );
2146
+ return () => {
2147
+ Scheduler.unstable_yieldValue(
2148
+ `Unmount normal [current: ${committedText}]`,
2149
+ );
2150
+ };
2151
});
2152
+ return null;
2153
+ }
2154
2401
- it('always re-computes if no inputs are provided', () => {
2402
- function LazyCompute(props) {
2403
- const computed = useMemo(props.compute);
2404
- return <Text text={computed} />;
2405
- }
2155
+ act(() => {
2156
+ ReactNoop.render(<Counter count={0} />, () =>
2157
+ Scheduler.unstable_yieldValue('Sync effect'),
2158
+ );
2159
+ expect(Scheduler).toFlushAndYieldThrough([
2160
+ 'Mount layout [current: 0]',
2161
+ 'Sync effect',
2162
+ ]);
2163
+ expect(committedText).toEqual('0');
2164
+ ReactNoop.render(<Counter count={1} />, () =>
2165
+ Scheduler.unstable_yieldValue('Sync effect'),
2166
+ );
2167
+ expect(Scheduler).toFlushAndYieldThrough([
2168
+ 'Mount normal [current: 0]',
2169
+ 'Unmount layout [current: 0]',
2170
+ 'Mount layout [current: 1]',
2171
+ 'Sync effect',
2172
+ ]);
2173
+ expect(committedText).toEqual('1');
2174
+ });
2175
2407
- function computeA() {
2408
- Scheduler.unstable_yieldValue('compute A');
2409
- return 'A';
2410
- }
2176
+ expect(Scheduler).toHaveYielded([
2177
+ 'Unmount normal [current: 1]',
2178
+ 'Mount normal [current: 1]',
2179
+ ]);
2180
+ });
2181
+ });
2182
2412
- function computeB() {
2413
- Scheduler.unstable_yieldValue('compute B');
2414
- return 'B';
2415
- }
2183
+ describe('useCallback', () => {
2184
+ it('memoizes callback by comparing inputs', () => {
2185
+ class IncrementButton extends React.PureComponent {
2186
+ increment = () => {
2187
+ this.props.increment();
2188
+ };
2189
+ render() {
2190
+ return <Text text="Increment" />;
2191
+ }
2192
+ }
2193
2417
- ReactNoop.render(<LazyCompute compute={computeA} />);
2418
- expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2194
+ function Counter({incrementBy}) {
2195
+ const [count, updateCount] = useState(0);
2196
+ const increment = useCallback(() => updateCount(c => c + incrementBy), [
2197
+ incrementBy,
2198
+ ]);
2199
+ return (
2200
+ <>
2201
+ <IncrementButton increment={increment} ref={button} />
2202
+ <Text text={'Count: ' + count} />
2203
+ </>
2204
+ );
2205
+ }
2206
2420
- ReactNoop.render(<LazyCompute compute={computeA} />);
2421
- expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2207
+ const button = React.createRef(null);
2208
+ ReactNoop.render(<Counter incrementBy={1} />);
2209
+ expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']);
2210
+ expect(ReactNoop.getChildren()).toEqual([
2211
+ span('Increment'),
2212
+ span('Count: 0'),
2213
+ ]);
2214
+
2215
+ act(button.current.increment);
2216
+ expect(Scheduler).toHaveYielded([
2217
+ // Button should not re-render, because its props haven't changed
2218
+ // 'Increment',
2219
+ 'Count: 1',
2220
+ ]);
2221
+ expect(ReactNoop.getChildren()).toEqual([
2222
+ span('Increment'),
2223
+ span('Count: 1'),
2224
+ ]);
2225
+
2226
+ // Increase the increment amount
2227
+ ReactNoop.render(<Counter incrementBy={10} />);
2228
+ expect(Scheduler).toFlushAndYield([
2229
+ // Inputs did change this time
2230
+ 'Increment',
2231
+ 'Count: 1',
2232
+ ]);
2233
+ expect(ReactNoop.getChildren()).toEqual([
2234
+ span('Increment'),
2235
+ span('Count: 1'),
2236
+ ]);
2237
+
2238
+ // Callback should have updated
2239
+ act(button.current.increment);
2240
+ expect(Scheduler).toHaveYielded(['Count: 11']);
2241
+ expect(ReactNoop.getChildren()).toEqual([
2242
+ span('Increment'),
2243
+ span('Count: 11'),
2244
+ ]);
2245
+ });
2246
+ });
2247
2423
- ReactNoop.render(<LazyCompute compute={computeA} />);
2424
- expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2248
+ describe('useMemo', () => {
2249
+ it('memoizes value by comparing to previous inputs', () => {
2250
+ function CapitalizedText(props) {
2251
+ const text = props.text;
2252
+ const capitalizedText = useMemo(() => {
2253
+ Scheduler.unstable_yieldValue(`Capitalize '${text}'`);
2254
+ return text.toUpperCase();
2255
+ }, [text]);
2256
+ return <Text text={capitalizedText} />;
2257
+ }
2258
2426
- ReactNoop.render(<LazyCompute compute={computeB} />);
2427
- expect(Scheduler).toFlushAndYield(['compute B', 'B']);
2428
- });
2259
+ ReactNoop.render(<CapitalizedText text="hello" />);
2260
+ expect(Scheduler).toFlushAndYield(["Capitalize 'hello'", 'HELLO']);
2261
+ expect(ReactNoop.getChildren()).toEqual([span('HELLO')]);
2262
2430
- it('should not invoke memoized function during re-renders unless inputs change', () => {
2431
- function LazyCompute(props) {
2432
- const computed = useMemo(() => props.compute(props.input), [
2433
- props.input,
2434
- ]);
2435
- const [count, setCount] = useState(0);
2436
- if (count < 3) {
2437
- setCount(count + 1);
2438
- }
2439
- return <Text text={computed} />;
2440
- }
2263
+ ReactNoop.render(<CapitalizedText text="hi" />);
2264
+ expect(Scheduler).toFlushAndYield(["Capitalize 'hi'", 'HI']);
2265
+ expect(ReactNoop.getChildren()).toEqual([span('HI')]);
2266
2442
- function compute(val) {
2443
- Scheduler.unstable_yieldValue('compute ' + val);
2444
- return val;
2445
- }
2267
+ ReactNoop.render(<CapitalizedText text="hi" />);
2268
+ expect(Scheduler).toFlushAndYield(['HI']);
2269
+ expect(ReactNoop.getChildren()).toEqual([span('HI')]);
2270
2447
- ReactNoop.render(<LazyCompute compute={compute} input="A" />);
2448
- expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2271
+ ReactNoop.render(<CapitalizedText text="goodbye" />);
2272
+ expect(Scheduler).toFlushAndYield(["Capitalize 'goodbye'", 'GOODBYE']);
2273
+ expect(ReactNoop.getChildren()).toEqual([span('GOODBYE')]);
2274
+ });
2275
2450
- ReactNoop.render(<LazyCompute compute={compute} input="A" />);
2451
- expect(Scheduler).toFlushAndYield(['A']);
2276
+ it('always re-computes if no inputs are provided', () => {
2277
+ function LazyCompute(props) {
2278
+ const computed = useMemo(props.compute);
2279
+ return <Text text={computed} />;
2280
+ }
2281
2453
- ReactNoop.render(<LazyCompute compute={compute} input="B" />);
2454
- expect(Scheduler).toFlushAndYield(['compute B', 'B']);
2455
- });
2456
- });
2282
+ function computeA() {
2283
+ Scheduler.unstable_yieldValue('compute A');
2284
+ return 'A';
2285
+ }
2286
2458
- describe('useRef', () => {
2459
- it('creates a ref object initialized with the provided value', () => {
2460
- jest.useFakeTimers();
2461
-
2462
- function useDebouncedCallback(callback, ms, inputs) {
2463
- const timeoutID = useRef(-1);
2464
- useEffect(() => {
2465
- return function unmount() {
2466
- clearTimeout(timeoutID.current);
2467
- };
2468
- }, []);
2469
- const debouncedCallback = useCallback(
2470
- (...args) => {
2471
- clearTimeout(timeoutID.current);
2472
- timeoutID.current = setTimeout(callback, ms, ...args);
2473
- },
2474
- [callback, ms],
2475
- );
2476
- return useCallback(debouncedCallback, inputs);
2477
- }
2287
+ function computeB() {
2288
+ Scheduler.unstable_yieldValue('compute B');
2289
+ return 'B';
2290
+ }
2291
2479
- let ping;
2480
- function App() {
2481
- ping = useDebouncedCallback(
2482
- value => {
2483
- Scheduler.unstable_yieldValue('ping: ' + value);
2484
- },
2485
- 100,
2486
- [],
2487
- );
2488
- return null;
2489
- }
2292
+ ReactNoop.render(<LazyCompute compute={computeA} />);
2293
+ expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2294
2491
- act(() => {
2492
- ReactNoop.render(<App />);
2493
- });
2494
- expect(Scheduler).toHaveYielded([]);
2295
+ ReactNoop.render(<LazyCompute compute={computeA} />);
2296
+ expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2297
2496
- ping(1);
2497
- ping(2);
2498
- ping(3);
2298
+ ReactNoop.render(<LazyCompute compute={computeA} />);
2299
+ expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2300
2500
- expect(Scheduler).toHaveYielded([]);
2301
+ ReactNoop.render(<LazyCompute compute={computeB} />);
2302
+ expect(Scheduler).toFlushAndYield(['compute B', 'B']);
2303
+ });
2304
2502
- jest.advanceTimersByTime(100);
2305
+ it('should not invoke memoized function during re-renders unless inputs change', () => {
2306
+ function LazyCompute(props) {
2307
+ const computed = useMemo(() => props.compute(props.input), [
2308
+ props.input,
2309
+ ]);
2310
+ const [count, setCount] = useState(0);
2311
+ if (count < 3) {
2312
+ setCount(count + 1);
2313
+ }
2314
+ return <Text text={computed} />;
2315
+ }
2316
2504
- expect(Scheduler).toHaveYielded(['ping: 3']);
2317
+ function compute(val) {
2318
+ Scheduler.unstable_yieldValue('compute ' + val);
2319
+ return val;
2320
+ }
2321
2506
- ping(4);
2507
- jest.advanceTimersByTime(20);
2508
- ping(5);
2509
- ping(6);
2510
- jest.advanceTimersByTime(80);
2322
+ ReactNoop.render(<LazyCompute compute={compute} input="A" />);
2323
+ expect(Scheduler).toFlushAndYield(['compute A', 'A']);
2324
2512
- expect(Scheduler).toHaveYielded([]);
2325
+ ReactNoop.render(<LazyCompute compute={compute} input="A" />);
2326
+ expect(Scheduler).toFlushAndYield(['A']);
2327
2514
- jest.advanceTimersByTime(20);
2515
- expect(Scheduler).toHaveYielded(['ping: 6']);
2516
- });
2328
+ ReactNoop.render(<LazyCompute compute={compute} input="B" />);
2329
+ expect(Scheduler).toFlushAndYield(['compute B', 'B']);
2330
+ });
2331
+ });
2332
2518
- it('should return the same ref during re-renders', () => {
2519
- function Counter() {
2520
- const ref = useRef('val');
2521
- const [count, setCount] = useState(0);
2522
- const [firstRef] = useState(ref);
2333
+ describe('useRef', () => {
2334
+ it('creates a ref object initialized with the provided value', () => {
2335
+ jest.useFakeTimers();
2336
2524
- if (firstRef !== ref) {
2525
- throw new Error('should never change');
2526
- }
2337
+ function useDebouncedCallback(callback, ms, inputs) {
2338
+ const timeoutID = useRef(-1);
2339
+ useEffect(() => {
2340
+ return function unmount() {
2341
+ clearTimeout(timeoutID.current);
2342
+ };
2343
+ }, []);
2344
+ const debouncedCallback = useCallback(
2345
+ (...args) => {
2346
+ clearTimeout(timeoutID.current);
2347
+ timeoutID.current = setTimeout(callback, ms, ...args);
2348
+ },
2349
+ [callback, ms],
2350
+ );
2351
+ return useCallback(debouncedCallback, inputs);
2352
+ }
2353
2528
- if (count < 3) {
2529
- setCount(count + 1);
2530
- }
2354
+ let ping;
2355
+ function App() {
2356
+ ping = useDebouncedCallback(
2357
+ value => {
2358
+ Scheduler.unstable_yieldValue('ping: ' + value);
2359
+ },
2360
+ 100,
2361
+ [],
2362
+ );
2363
+ return null;
2364
+ }
2365
2532
- return <Text text={ref.current} />;
2533
- }
2366
+ act(() => {
2367
+ ReactNoop.render(<App />);
2368
+ });
2369
+ expect(Scheduler).toHaveYielded([]);
2370
2535
- ReactNoop.render(<Counter />);
2536
- expect(Scheduler).toFlushAndYield(['val']);
2371
+ ping(1);
2372
+ ping(2);
2373
+ ping(3);
2374
2538
- ReactNoop.render(<Counter />);
2539
- expect(Scheduler).toFlushAndYield(['val']);
2540
- });
2541
- });
2375
+ expect(Scheduler).toHaveYielded([]);
2376
2543
- describe('useImperativeHandle', () => {
2544
- it('does not update when deps are the same', () => {
2545
- const INCREMENT = 'INCREMENT';
2377
+ jest.advanceTimersByTime(100);
2378
2547
- function reducer(state, action) {
2548
- return action === INCREMENT ? state + 1 : state;
2549
- }
2379
+ expect(Scheduler).toHaveYielded(['ping: 3']);
2380
2551
- function Counter(props, ref) {
2552
- const [count, dispatch] = useReducer(reducer, 0);
2553
- useImperativeHandle(ref, () => ({count, dispatch}), []);
2554
- return <Text text={'Count: ' + count} />;
2555
- }
2381
+ ping(4);
2382
+ jest.advanceTimersByTime(20);
2383
+ ping(5);
2384
+ ping(6);
2385
+ jest.advanceTimersByTime(80);
2386
2557
- Counter = forwardRef(Counter);
2558
- const counter = React.createRef(null);
2559
- ReactNoop.render(<Counter ref={counter} />);
2560
- expect(Scheduler).toFlushAndYield(['Count: 0']);
2561
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2562
- expect(counter.current.count).toBe(0);
2387
+ expect(Scheduler).toHaveYielded([]);
2388
2564
- act(() => {
2565
- counter.current.dispatch(INCREMENT);
2566
- });
2567
- expect(Scheduler).toHaveYielded(['Count: 1']);
2568
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2569
- // Intentionally not updated because of [] deps:
2570
- expect(counter.current.count).toBe(0);
2571
- });
2389
+ jest.advanceTimersByTime(20);
2390
+ expect(Scheduler).toHaveYielded(['ping: 6']);
2391
+ });
2392
2573
- // Regression test for https://github.com/facebook/react/issues/14782
2574
- it('automatically updates when deps are not specified', () => {
2575
- const INCREMENT = 'INCREMENT';
2393
+ it('should return the same ref during re-renders', () => {
2394
+ function Counter() {
2395
+ const ref = useRef('val');
2396
+ const [count, setCount] = useState(0);
2397
+ const [firstRef] = useState(ref);
2398
2577
- function reducer(state, action) {
2578
- return action === INCREMENT ? state + 1 : state;
2579
- }
2399
+ if (firstRef !== ref) {
2400
+ throw new Error('should never change');
2401
+ }
2402
2581
- function Counter(props, ref) {
2582
- const [count, dispatch] = useReducer(reducer, 0);
2583
- useImperativeHandle(ref, () => ({count, dispatch}));
2584
- return <Text text={'Count: ' + count} />;
2585
- }
2403
+ if (count < 3) {
2404
+ setCount(count + 1);
2405
+ }
2406
2587
- Counter = forwardRef(Counter);
2588
- const counter = React.createRef(null);
2589
- ReactNoop.render(<Counter ref={counter} />);
2590
- expect(Scheduler).toFlushAndYield(['Count: 0']);
2591
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2592
- expect(counter.current.count).toBe(0);
2407
+ return <Text text={ref.current} />;
2408
+ }
2409
2594
- act(() => {
2595
- counter.current.dispatch(INCREMENT);
2596
- });
2597
- expect(Scheduler).toHaveYielded(['Count: 1']);
2598
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2599
- expect(counter.current.count).toBe(1);
2600
- });
2410
+ ReactNoop.render(<Counter />);
2411
+ expect(Scheduler).toFlushAndYield(['val']);
2412
2602
- it('updates when deps are different', () => {
2603
- const INCREMENT = 'INCREMENT';
2413
+ ReactNoop.render(<Counter />);
2414
+ expect(Scheduler).toFlushAndYield(['val']);
2415
+ });
2416
+ });
2417
2605
- function reducer(state, action) {
2606
- return action === INCREMENT ? state + 1 : state;
2607
- }
2418
+ describe('useImperativeHandle', () => {
2419
+ it('does not update when deps are the same', () => {
2420
+ const INCREMENT = 'INCREMENT';
2421
2609
- let totalRefUpdates = 0;
2610
- function Counter(props, ref) {
2611
- const [count, dispatch] = useReducer(reducer, 0);
2612
- useImperativeHandle(
2613
- ref,
2614
- () => {
2615
- totalRefUpdates++;
2616
- return {count, dispatch};
2617
- },
2618
- [count],
2619
- );
2620
- return <Text text={'Count: ' + count} />;
2621
- }
2422
+ function reducer(state, action) {
2423
+ return action === INCREMENT ? state + 1 : state;
2424
+ }
2425
2623
- Counter = forwardRef(Counter);
2624
- const counter = React.createRef(null);
2625
- ReactNoop.render(<Counter ref={counter} />);
2626
- expect(Scheduler).toFlushAndYield(['Count: 0']);
2627
- expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2628
- expect(counter.current.count).toBe(0);
2629
- expect(totalRefUpdates).toBe(1);
2426
+ function Counter(props, ref) {
2427
+ const [count, dispatch] = useReducer(reducer, 0);
2428
+ useImperativeHandle(ref, () => ({count, dispatch}), []);
2429
+ return <Text text={'Count: ' + count} />;
2430
+ }
2431
2631
- act(() => {
2632
- counter.current.dispatch(INCREMENT);
2633
- });
2634
- expect(Scheduler).toHaveYielded(['Count: 1']);
2635
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2636
- expect(counter.current.count).toBe(1);
2637
- expect(totalRefUpdates).toBe(2);
2432
+ Counter = forwardRef(Counter);
2433
+ const counter = React.createRef(null);
2434
+ ReactNoop.render(<Counter ref={counter} />);
2435
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
2436
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2437
+ expect(counter.current.count).toBe(0);
2438
2639
- // Update that doesn't change the ref dependencies
2640
- ReactNoop.render(<Counter ref={counter} />);
2641
- expect(Scheduler).toFlushAndYield(['Count: 1']);
2642
- expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2643
- expect(counter.current.count).toBe(1);
2644
- expect(totalRefUpdates).toBe(2); // Should not increase since last time
2645
- });
2646
- });
2647
- describe('useTransition', () => {
2648
- it.experimental(
2649
- 'delays showing loading state until after timeout',
2650
- async () => {
2651
- let transition;
2652
- function App() {
2653
- const [show, setShow] = useState(false);
2654
- const [startTransition, isPending] = useTransition({
2655
- timeoutMs: 1000,
2656
- });
2657
- transition = () => {
2658
- startTransition(() => {
2659
- setShow(true);
2660
- });
2661
- };
2662
- return (
2663
- <Suspense
2664
- fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
2665
- {show ? (
2666
- <AsyncText
2667
- ms={2000}
2668
- text={`After... Pending: ${isPending}`}
2669
- />
2670
- ) : (
2671
- <Text text={`Before... Pending: ${isPending}`} />
2672
- )}
2673
- </Suspense>
2674
- );
2675
- }
2676
- ReactNoop.render(<App />);
2677
- expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
2678
- expect(ReactNoop.getChildren()).toEqual([
2679
- span('Before... Pending: false'),
2680
- ]);
2681
-
2682
- act(() => {
2683
- Scheduler.unstable_runWithPriority(
2684
- Scheduler.unstable_UserBlockingPriority,
2685
- transition,
2686
- );
2687
- });
2688
- Scheduler.unstable_advanceTime(500);
2689
- await advanceTimers(500);
2690
- expect(Scheduler).toHaveYielded([
2691
- 'Before... Pending: true',
2692
- 'Suspend! [After... Pending: false]',
2693
- 'Loading... Pending: false',
2694
- ]);
2695
- expect(ReactNoop.getChildren()).toEqual([
2696
- span('Before... Pending: true'),
2697
- ]);
2698
-
2699
- Scheduler.unstable_advanceTime(1000);
2700
- await advanceTimers(1000);
2701
- expect(ReactNoop.getChildren()).toEqual([
2702
- hiddenSpan('Before... Pending: true'),
2703
- span('Loading... Pending: false'),
2704
- ]);
2705
-
2706
- Scheduler.unstable_advanceTime(500);
2707
- await advanceTimers(500);
2708
- expect(Scheduler).toHaveYielded([
2709
- 'Promise resolved [After... Pending: false]',
2710
- ]);
2711
- expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2712
- expect(ReactNoop.getChildren()).toEqual([
2713
- span('After... Pending: false'),
2714
- ]);
2715
- },
2716
- );
2717
- it.experimental(
2718
- 'delays showing loading state until after busyDelayMs + busyMinDurationMs',
2719
- async () => {
2720
- let transition;
2721
- function App() {
2722
- const [show, setShow] = useState(false);
2723
- const [startTransition, isPending] = useTransition({
2724
- busyDelayMs: 1000,
2725
- busyMinDurationMs: 2000,
2726
- });
2727
- transition = () => {
2728
- startTransition(() => {
2729
- setShow(true);
2730
- });
2731
- };
2732
- return (
2733
- <Suspense
2734
- fallback={<Text text={`Loading... Pending: ${isPending}`} />}>
2735
- {show ? (
2736
- <AsyncText
2737
- ms={2000}
2738
- text={`After... Pending: ${isPending}`}
2739
- />
2740
- ) : (
2741
- <Text text={`Before... Pending: ${isPending}`} />
2742
- )}
2743
- </Suspense>
2744
- );
2745
- }
2746
- ReactNoop.render(<App />);
2747
- expect(Scheduler).toFlushAndYield(['Before... Pending: false']);
2748
- expect(ReactNoop.getChildren()).toEqual([
2749
- span('Before... Pending: false'),
2750
- ]);
2751
-
2752
- act(() => {
2753
- Scheduler.unstable_runWithPriority(
2754
- Scheduler.unstable_UserBlockingPriority,
2755
- transition,
2756
- );
2757
- });
2758
- Scheduler.unstable_advanceTime(1000);
2759
- await advanceTimers(1000);
2760
- expect(Scheduler).toHaveYielded([
2761
- 'Before... Pending: true',
2762
- 'Suspend! [After... Pending: false]',
2763
- 'Loading... Pending: false',
2764
- ]);
2765
- expect(ReactNoop.getChildren()).toEqual([
2766
- span('Before... Pending: true'),
2767
- ]);
2768
-
2769
- Scheduler.unstable_advanceTime(1000);
2770
- await advanceTimers(1000);
2771
- expect(Scheduler).toHaveYielded([
2772
- 'Promise resolved [After... Pending: false]',
2773
- ]);
2774
- expect(Scheduler).toFlushAndYield(['After... Pending: false']);
2775
- expect(ReactNoop.getChildren()).toEqual([
2776
- span('Before... Pending: true'),
2777
- ]);
2778
-
2779
- Scheduler.unstable_advanceTime(1000);
2780
- await advanceTimers(1000);
2781
- expect(ReactNoop.getChildren()).toEqual([
2782
- span('Before... Pending: true'),
2783
- ]);
2784
- Scheduler.unstable_advanceTime(250);
2785
- await advanceTimers(250);
2786
- expect(ReactNoop.getChildren()).toEqual([
2787
- span('After... Pending: false'),
2788
- ]);
2789
- },
2790
- );
2439
+ act(() => {
2440
+ counter.current.dispatch(INCREMENT);
2441
});
2792
- describe('useDeferredValue', () => {
2793
- it.experimental(
2794
- 'defers text value until specified timeout',
2795
- async () => {
2796
- function TextBox({text}) {
2797
- return <AsyncText ms={1000} text={text} />;
2798
- }
2799
-
2800
- let _setText;
2801
- function App() {
2802
- const [text, setText] = useState('A');
2803
- const deferredText = useDeferredValue(text, {
2804
- timeoutMs: 500,
2805
- });
2806
- _setText = setText;
2807
- return (
2808
- <>
2809
- <Text text={text} />
2810
- <Suspense fallback={<Text text={'Loading'} />}>
2811
- <TextBox text={deferredText} />
2812
- </Suspense>
2813
- </>
2814
- );
2815
- }
2442
+ expect(Scheduler).toHaveYielded(['Count: 1']);
2443
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
2444
+ // Intentionally not updated because of [] deps:
2445
+ expect(counter.current.count).toBe(0);
2446
+ });
2447
2817
- act(() => {
2818
- ReactNoop.render(<App />);
2819
- });
2448
+ // Regression test for https://github.com/facebook/react/issues/14782
2449
+ it('automatically updates when deps are not specified', () => {
2450
+ const INCREMENT = 'INCREMENT';
2451
2821
- expect(Scheduler).toHaveYielded(['A', 'Suspend! [A]', 'Loading']);
2822
- expect(ReactNoop.getChildren()).toEqual([
2823
- span('A'),
2824
- span('Loading'),
2825
- ]);
2452
+ function reducer(state, action) {
2453
+ return action === INCREMENT ? state + 1 : state;
2454
+ }
2455
2827
- Scheduler.unstable_advanceTime(1000);
2828
- await advanceTimers(1000);
2829
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2830
- expect(Scheduler).toFlushAndYield(['A']);
2831
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('A')]);
2456
+ function Counter(props, ref) {
2457
+ const [count, dispatch] = useReducer(reducer, 0);
2458
+ useImperativeHandle(ref, () => ({count, dispatch}));
2459
+ return <Text text={'Count: ' + count} />;
2460
+ }
2461
2833
- act(() => {
2834
- _setText('B');
2835
- });
2836
- expect(Scheduler).toHaveYielded([
2837
- 'B',
2838
- 'A',
2839
- 'B',
2840
- 'Suspend! [B]',
2841
- 'Loading',
2842
- ]);
2843
- expect(Scheduler).toFlushAndYield([]);
2844
- expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2462
+ Counter = forwardRef(Counter);
2463
+ const counter = React.createRef(null);
2464
+ ReactNoop.render(<Counter ref={counter} />);
2465
+ expect(Scheduler).toFlushAndYield(['Count: 0']);
2466
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
2467
+ expect(counter.current.count).toBe(0);
2468
2846
- Scheduler.unstable_advanceTime(250);
2847
- await advanceTimers(250);
2848
- expect(Scheduler).toFlushAndYield([]);
2849
- expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]);
2850
-
2851
- Scheduler.unstable_advanceTime(500);
2852
- await advanceTimers(500);
2853
- expect(ReactNoop.getChildren()).toEqual([
2854
- span('B'),
2855
- hiddenSpan('A'),
2856
- span('Loading'),
2857
- ]);
2858
-
2859
- Scheduler.unstable_advanceTime(250);
2860
- await advanceTimers(250);
2861
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2862
-
2863
- act(() => {
2864
- expect(Scheduler).toFlushAndYield(['B']);
2865
- });
2866
- expect(ReactNoop.getChildren()).toEqual([span('B'), span('B')]);
2867
- },
2868
- );
2469
+ act(() => {
2470
+ counter.current.dispatch(INCREMENT);
2471
});
2472
+ expect(Scheduler).toHaveYielded(['Count: 1']);
This file is too large to show in full.
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+2681
-2738
@@ -9,1615 +9,1211 @@ let Suspense;
9
let TextResource;
10
let textResourceShouldFail;
11
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
- );
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;
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
+
21
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
22
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
23
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
24
+ ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
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('does not restart rendering for initial render', async () => {
100
+ function Bar(props) {
101
+ Scheduler.unstable_yieldValue('Bar');
102
+ return props.children;
103
+ }
104
+
105
+ function Foo() {
106
+ Scheduler.unstable_yieldValue('Foo');
107
+ return (
108
+ <>
109
+ <Suspense fallback={<Text text="Loading..." />}>
110
+ <Bar>
111
+ <AsyncText text="A" ms={100} />
112
+ <Text text="B" />
113
+ </Bar>
114
+ </Suspense>
115
+ <Text text="C" />
116
+ <Text text="D" />
117
+ </>
118
+ );
119
+ }
120
+
121
+ ReactNoop.render(<Foo />);
122
+ expect(Scheduler).toFlushAndYieldThrough([
123
+ 'Foo',
124
+ 'Bar',
125
+ // A suspends
126
+ 'Suspend! [A]',
127
+ // But we keep rendering the siblings
128
+ 'B',
129
+ 'Loading...',
130
+ 'C',
131
+ // We leave D incomplete.
132
+ ]);
133
+ expect(ReactNoop.getChildren()).toEqual([]);
134
+
135
+ // Flush the promise completely
136
+ Scheduler.unstable_advanceTime(100);
137
+ await advanceTimers(100);
138
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
139
+
140
+ // Even though the promise has resolved, we should now flush
141
+ // and commit the in progress render instead of restarting.
142
+ expect(Scheduler).toFlushAndYield(['D']);
143
+ expect(ReactNoop.getChildren()).toEqual([
144
+ span('Loading...'),
145
+ span('C'),
146
+ span('D'),
147
+ ]);
148
+
149
+ // Await one micro task to attach the retry listeners.
150
+ await null;
151
+
152
+ // Next, we'll flush the complete content.
153
+ expect(Scheduler).toFlushAndYield(['Bar', 'A', 'B']);
154
+
155
+ expect(ReactNoop.getChildren()).toEqual([
156
+ span('A'),
157
+ span('B'),
158
+ span('C'),
159
+ span('D'),
160
+ ]);
161
+ });
162
56
- beforeEach(() => {
57
- jest.resetModules();
163
+ it('suspends rendering and continues later', async () => {
164
+ function Bar(props) {
165
+ Scheduler.unstable_yieldValue('Bar');
166
+ return props.children;
167
+ }
168
+
169
+ function Foo({renderBar}) {
170
+ Scheduler.unstable_yieldValue('Foo');
171
+ return (
172
+ <Suspense fallback={<Text text="Loading..." />}>
173
+ {renderBar ? (
174
+ <Bar>
175
+ <AsyncText text="A" ms={100} />
176
+ <Text text="B" />
177
+ </Bar>
178
+ ) : null}
179
+ </Suspense>
180
+ );
181
+ }
182
+
183
+ // Render empty shell.
184
+ ReactNoop.render(<Foo />);
185
+ expect(Scheduler).toFlushAndYield(['Foo']);
186
+
187
+ // The update will suspend.
188
+ ReactNoop.render(<Foo renderBar={true} />);
189
+ expect(Scheduler).toFlushAndYield([
190
+ 'Foo',
191
+ 'Bar',
192
+ // A suspends
193
+ 'Suspend! [A]',
194
+ // But we keep rendering the siblings
195
+ 'B',
196
+ 'Loading...',
197
+ ]);
198
+ expect(ReactNoop.getChildren()).toEqual([]);
199
+
200
+ // Flush some of the time
201
+ await advanceTimers(50);
202
+ // Still nothing...
203
+ expect(Scheduler).toFlushWithoutYielding();
204
+ expect(ReactNoop.getChildren()).toEqual([]);
205
+
206
+ // Flush the promise completely
207
+ await advanceTimers(50);
208
+ // Renders successfully
209
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
210
+ expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'A', 'B']);
211
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
212
+ });
213
59
- loadModules({
60
- deferPassiveEffectCleanupDuringUnmount,
61
- runAllPassiveEffectDestroysBeforeCreates,
62
- });
63
- });
214
+ it('suspends siblings and later recovers each independently', async () => {
215
+ // Render two sibling Suspense components
216
+ ReactNoop.render(
217
+ <Fragment>
218
+ <Suspense fallback={<Text text="Loading A..." />}>
219
+ <AsyncText text="A" ms={5000} />
220
+ </Suspense>
221
+ <Suspense fallback={<Text text="Loading B..." />}>
222
+ <AsyncText text="B" ms={6000} />
223
+ </Suspense>
224
+ </Fragment>,
225
+ );
226
+ expect(Scheduler).toFlushAndYield([
227
+ 'Suspend! [A]',
228
+ 'Loading A...',
229
+ 'Suspend! [B]',
230
+ 'Loading B...',
231
+ ]);
232
+ expect(ReactNoop.getChildren()).toEqual([
233
+ span('Loading A...'),
234
+ span('Loading B...'),
235
+ ]);
236
+
237
+ // Advance time by enough that the first Suspense's promise resolves and
238
+ // switches back to the normal view. The second Suspense should still
239
+ // show the placeholder
240
+ ReactNoop.expire(5000);
241
+ await advanceTimers(5000);
242
+
243
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
244
+ expect(Scheduler).toFlushAndYield(['A']);
245
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
246
+
247
+ // Advance time by enough that the second Suspense's promise resolves
248
+ // and switches back to the normal view
249
+ ReactNoop.expire(1000);
250
+ await advanceTimers(1000);
251
+
252
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
253
+ expect(Scheduler).toFlushAndYield(['B']);
254
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
255
+ });
256
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
- // }
257
+ it('continues rendering siblings after suspending', async () => {
258
+ // A shell is needed. The update cause it to suspend.
259
+ ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
260
+ expect(Scheduler).toFlushAndYield([]);
261
+ // B suspends. Continue rendering the remaining siblings.
262
+ ReactNoop.render(
263
+ <Suspense fallback={<Text text="Loading..." />}>
264
+ <Text text="A" />
265
+ <AsyncText text="B" />
266
+ <Text text="C" />
267
+ <Text text="D" />
268
+ </Suspense>,
269
+ );
270
+ // B suspends. Continue rendering the remaining siblings.
271
+ expect(Scheduler).toFlushAndYield([
272
+ 'A',
273
+ 'Suspend! [B]',
274
+ 'C',
275
+ 'D',
276
+ 'Loading...',
277
+ ]);
278
+ // Did not commit yet.
279
+ expect(ReactNoop.getChildren()).toEqual([]);
280
+
281
+ // Wait for data to resolve
282
+ await advanceTimers(100);
283
+ // Renders successfully
284
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
285
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'C', 'D']);
286
+ expect(ReactNoop.getChildren()).toEqual([
287
+ span('A'),
288
+ span('B'),
289
+ span('C'),
290
+ span('D'),
291
+ ]);
292
+ });
293
72
- function span(prop) {
73
- return {type: 'span', children: [], prop, hidden: false};
294
+ it('retries on error', async () => {
295
+ class ErrorBoundary extends React.Component {
296
+ state = {error: null};
297
+ componentDidCatch(error) {
298
+ this.setState({error});
299
}
75
-
76
- function hiddenSpan(prop) {
77
- return {type: 'span', children: [], prop, hidden: true};
300
+ reset() {
301
+ this.setState({error: null});
302
}
79
-
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');
303
+ render() {
304
+ if (this.state.error !== null) {
305
+ return <Text text={'Caught error: ' + this.state.error.message} />;
306
}
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
- }
91
-
92
- function Text(props) {
93
- Scheduler.unstable_yieldValue(props.text);
94
- return <span prop={props.text} ref={props.hostRef} />;
307
+ return this.props.children;
308
}
309
+ }
310
+
311
+ const errorBoundary = React.createRef();
312
+ function App({renderContent}) {
313
+ return (
314
+ <Suspense fallback={<Text text="Loading..." />}>
315
+ {renderContent ? (
316
+ <ErrorBoundary ref={errorBoundary}>
317
+ <AsyncText text="Result" ms={1000} />
318
+ </ErrorBoundary>
319
+ ) : null}
320
+ </Suspense>
321
+ );
322
+ }
323
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
- }
111
- }
112
-
113
- it('does not restart rendering for initial render', async () => {
114
- function Bar(props) {
115
- Scheduler.unstable_yieldValue('Bar');
116
- return props.children;
117
- }
118
-
119
- function Foo() {
120
- Scheduler.unstable_yieldValue('Foo');
121
- return (
122
- <>
123
- <Suspense fallback={<Text text="Loading..." />}>
124
- <Bar>
125
- <AsyncText text="A" ms={100} />
126
- <Text text="B" />
127
- </Bar>
128
- </Suspense>
129
- <Text text="C" />
130
- <Text text="D" />
131
- </>
132
- );
133
- }
134
-
135
- ReactNoop.render(<Foo />);
136
- expect(Scheduler).toFlushAndYieldThrough([
137
- 'Foo',
138
- 'Bar',
139
- // A suspends
140
- 'Suspend! [A]',
141
- // But we keep rendering the siblings
142
- 'B',
143
- 'Loading...',
144
- 'C',
145
- // We leave D incomplete.
146
- ]);
147
- expect(ReactNoop.getChildren()).toEqual([]);
148
-
149
- // Flush the promise completely
150
- Scheduler.unstable_advanceTime(100);
151
- await advanceTimers(100);
152
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
153
-
154
- // Even though the promise has resolved, we should now flush
155
- // and commit the in progress render instead of restarting.
156
- expect(Scheduler).toFlushAndYield(['D']);
157
- expect(ReactNoop.getChildren()).toEqual([
158
- span('Loading...'),
159
- span('C'),
160
- span('D'),
161
- ]);
162
-
163
- // Await one micro task to attach the retry listeners.
164
- await null;
165
-
166
- // Next, we'll flush the complete content.
167
- expect(Scheduler).toFlushAndYield(['Bar', 'A', 'B']);
168
-
169
- expect(ReactNoop.getChildren()).toEqual([
170
- span('A'),
171
- span('B'),
172
- span('C'),
173
- span('D'),
174
- ]);
175
- });
176
-
177
- it('suspends rendering and continues later', async () => {
178
- function Bar(props) {
179
- Scheduler.unstable_yieldValue('Bar');
180
- return props.children;
181
- }
182
-
183
- function Foo({renderBar}) {
184
- Scheduler.unstable_yieldValue('Foo');
185
- return (
186
- <Suspense fallback={<Text text="Loading..." />}>
187
- {renderBar ? (
188
- <Bar>
189
- <AsyncText text="A" ms={100} />
190
- <Text text="B" />
191
- </Bar>
192
- ) : null}
193
- </Suspense>
194
- );
195
- }
196
-
197
- // Render empty shell.
198
- ReactNoop.render(<Foo />);
199
- expect(Scheduler).toFlushAndYield(['Foo']);
200
-
201
- // The update will suspend.
202
- ReactNoop.render(<Foo renderBar={true} />);
203
- expect(Scheduler).toFlushAndYield([
204
- 'Foo',
205
- 'Bar',
206
- // A suspends
207
- 'Suspend! [A]',
208
- // But we keep rendering the siblings
209
- 'B',
210
- 'Loading...',
211
- ]);
212
- expect(ReactNoop.getChildren()).toEqual([]);
213
-
214
- // Flush some of the time
215
- await advanceTimers(50);
216
- // Still nothing...
217
- expect(Scheduler).toFlushWithoutYielding();
218
- expect(ReactNoop.getChildren()).toEqual([]);
219
-
220
- // Flush the promise completely
221
- await advanceTimers(50);
222
- // Renders successfully
223
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
224
- expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'A', 'B']);
225
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
226
- });
227
-
228
- it('suspends siblings and later recovers each independently', async () => {
229
- // Render two sibling Suspense components
230
- ReactNoop.render(
231
- <Fragment>
232
- <Suspense fallback={<Text text="Loading A..." />}>
233
- <AsyncText text="A" ms={5000} />
234
- </Suspense>
235
- <Suspense fallback={<Text text="Loading B..." />}>
236
- <AsyncText text="B" ms={6000} />
237
- </Suspense>
238
- </Fragment>,
239
- );
240
- expect(Scheduler).toFlushAndYield([
241
- 'Suspend! [A]',
242
- 'Loading A...',
243
- 'Suspend! [B]',
244
- 'Loading B...',
245
- ]);
246
- expect(ReactNoop.getChildren()).toEqual([
247
- span('Loading A...'),
248
- span('Loading B...'),
249
- ]);
250
-
251
- // Advance time by enough that the first Suspense's promise resolves and
252
- // switches back to the normal view. The second Suspense should still
253
- // show the placeholder
254
- ReactNoop.expire(5000);
255
- await advanceTimers(5000);
256
-
257
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
258
- expect(Scheduler).toFlushAndYield(['A']);
259
- expect(ReactNoop.getChildren()).toEqual([
260
- span('A'),
261
- span('Loading B...'),
262
- ]);
263
-
264
- // Advance time by enough that the second Suspense's promise resolves
265
- // and switches back to the normal view
266
- ReactNoop.expire(1000);
267
- await advanceTimers(1000);
268
-
269
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
270
- expect(Scheduler).toFlushAndYield(['B']);
271
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
272
- });
273
-
274
- it('continues rendering siblings after suspending', async () => {
275
- // A shell is needed. The update cause it to suspend.
276
- ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
277
- expect(Scheduler).toFlushAndYield([]);
278
- // B suspends. Continue rendering the remaining siblings.
279
- ReactNoop.render(
280
- <Suspense fallback={<Text text="Loading..." />}>
281
- <Text text="A" />
282
- <AsyncText text="B" />
283
- <Text text="C" />
284
- <Text text="D" />
285
- </Suspense>,
286
- );
287
- // B suspends. Continue rendering the remaining siblings.
288
- expect(Scheduler).toFlushAndYield([
289
- 'A',
290
- 'Suspend! [B]',
291
- 'C',
292
- 'D',
293
- 'Loading...',
294
- ]);
295
- // Did not commit yet.
296
- expect(ReactNoop.getChildren()).toEqual([]);
297
-
298
- // Wait for data to resolve
299
- await advanceTimers(100);
300
- // Renders successfully
301
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
302
- expect(Scheduler).toFlushAndYield(['A', 'B', 'C', 'D']);
303
- expect(ReactNoop.getChildren()).toEqual([
304
- span('A'),
305
- span('B'),
306
- span('C'),
307
- span('D'),
308
- ]);
309
- });
310
-
311
- it('retries on error', async () => {
312
- class ErrorBoundary extends React.Component {
313
- state = {error: null};
314
- componentDidCatch(error) {
315
- this.setState({error});
316
- }
317
- reset() {
318
- this.setState({error: null});
319
- }
320
- render() {
321
- if (this.state.error !== null) {
322
- return (
323
- <Text text={'Caught error: ' + this.state.error.message} />
324
- );
325
- }
326
- return this.props.children;
327
- }
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
- }
342
-
343
- ReactNoop.render(<App />);
344
- expect(Scheduler).toFlushAndYield([]);
345
- expect(ReactNoop.getChildren()).toEqual([]);
346
-
347
- ReactNoop.render(<App renderContent={true} />);
348
- expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
349
- expect(ReactNoop.getChildren()).toEqual([]);
324
+ ReactNoop.render(<App />);
325
+ expect(Scheduler).toFlushAndYield([]);
326
+ expect(ReactNoop.getChildren()).toEqual([]);
327
351
- textResourceShouldFail = true;
352
- ReactNoop.expire(1000);
353
- await advanceTimers(1000);
354
- textResourceShouldFail = false;
328
+ ReactNoop.render(<App renderContent={true} />);
329
+ expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
330
+ expect(ReactNoop.getChildren()).toEqual([]);
331
356
- expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
332
+ textResourceShouldFail = true;
333
+ ReactNoop.expire(1000);
334
+ await advanceTimers(1000);
335
+ textResourceShouldFail = false;
336
358
- expect(Scheduler).toFlushAndYield([
359
- 'Error! [Result]',
337
+ expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
338
361
- // React retries one more time
362
- 'Error! [Result]',
339
+ expect(Scheduler).toFlushAndYield([
340
+ 'Error! [Result]',
341
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
- });
342
+ // React retries one more time
343
+ 'Error! [Result]',
344
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 (
384
- <Text text={'Caught error: ' + this.state.error.message} />
385
- );
386
- }
387
- return this.props.children;
388
- }
389
- }
345
+ // Errored again on retry. Now handle it.
346
+ 'Caught error: Failed to load: Result',
347
+ ]);
348
+ expect(ReactNoop.getChildren()).toEqual([
349
+ span('Caught error: Failed to load: Result'),
350
+ ]);
351
+ });
352
391
- const errorBoundary = React.createRef();
392
- function App() {
393
- return (
394
- <Suspense fallback={<Text text="Loading..." />}>
395
- <ErrorBoundary ref={errorBoundary}>
396
- <AsyncText text="Result" ms={3000} />
397
- </ErrorBoundary>
398
- </Suspense>
399
- );
353
+ it('retries on error after falling back to a placeholder', async () => {
354
+ class ErrorBoundary extends React.Component {
355
+ state = {error: null};
356
+ componentDidCatch(error) {
357
+ this.setState({error});
358
+ }
359
+ reset() {
360
+ this.setState({error: null});
361
+ }
362
+ render() {
363
+ if (this.state.error !== null) {
364
+ return <Text text={'Caught error: ' + this.state.error.message} />;
365
}
366
+ return this.props.children;
367
+ }
368
+ }
369
+
370
+ const errorBoundary = React.createRef();
371
+ function App() {
372
+ return (
373
+ <Suspense fallback={<Text text="Loading..." />}>
374
+ <ErrorBoundary ref={errorBoundary}>
375
+ <AsyncText text="Result" ms={3000} />
376
+ </ErrorBoundary>
377
+ </Suspense>
378
+ );
379
+ }
380
402
- ReactNoop.render(<App />);
403
- expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
404
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
405
-
406
- textResourceShouldFail = true;
407
- ReactNoop.expire(3000);
408
- await advanceTimers(3000);
409
- textResourceShouldFail = false;
381
+ ReactNoop.render(<App />);
382
+ expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']);
383
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
384
411
- expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
412
- expect(Scheduler).toFlushAndYield([
413
- 'Error! [Result]',
385
+ textResourceShouldFail = true;
386
+ ReactNoop.expire(3000);
387
+ await advanceTimers(3000);
388
+ textResourceShouldFail = false;
389
415
- // React retries one more time
416
- 'Error! [Result]',
390
+ expect(Scheduler).toHaveYielded(['Promise rejected [Result]']);
391
+ expect(Scheduler).toFlushAndYield([
392
+ 'Error! [Result]',
393
418
- // Errored again on retry. Now handle it.
394
+ // React retries one more time
395
+ 'Error! [Result]',
396
420
- 'Caught error: Failed to load: Result',
421
- ]);
422
- expect(ReactNoop.getChildren()).toEqual([
423
- span('Caught error: Failed to load: Result'),
424
- ]);
425
- });
397
+ // Errored again on retry. Now handle it.
398
427
- it('can update at a higher priority while in a suspended state', async () => {
428
- function App(props) {
429
- return (
430
- <Suspense fallback={<Text text="Loading..." />}>
431
- <Text text={props.highPri} />
432
- <AsyncText text={props.lowPri} />
433
- </Suspense>
434
- );
435
- }
399
+ 'Caught error: Failed to load: Result',
400
+ ]);
401
+ expect(ReactNoop.getChildren()).toEqual([
402
+ span('Caught error: Failed to load: Result'),
403
+ ]);
404
+ });
405
437
- // Initial mount
438
- ReactNoop.render(<App highPri="A" lowPri="1" />);
439
- expect(Scheduler).toFlushAndYield(['A', 'Suspend! [1]', 'Loading...']);
440
- await advanceTimers(0);
441
- expect(Scheduler).toHaveYielded(['Promise resolved [1]']);
442
- expect(Scheduler).toFlushAndYield(['A', '1']);
443
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('1')]);
444
-
445
- // Update the low-pri text
446
- ReactNoop.render(<App highPri="A" lowPri="2" />);
447
- expect(Scheduler).toFlushAndYield([
448
- 'A',
449
- // Suspends
450
- 'Suspend! [2]',
451
- 'Loading...',
452
- ]);
406
+ it('can update at a higher priority while in a suspended state', async () => {
407
+ function App(props) {
408
+ return (
409
+ <Suspense fallback={<Text text="Loading..." />}>
410
+ <Text text={props.highPri} />
411
+ <AsyncText text={props.lowPri} />
412
+ </Suspense>
413
+ );
414
+ }
415
+
416
+ // Initial mount
417
+ ReactNoop.render(<App highPri="A" lowPri="1" />);
418
+ expect(Scheduler).toFlushAndYield(['A', 'Suspend! [1]', 'Loading...']);
419
+ await advanceTimers(0);
420
+ expect(Scheduler).toHaveYielded(['Promise resolved [1]']);
421
+ expect(Scheduler).toFlushAndYield(['A', '1']);
422
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('1')]);
423
+
424
+ // Update the low-pri text
425
+ ReactNoop.render(<App highPri="A" lowPri="2" />);
426
+ expect(Scheduler).toFlushAndYield([
427
+ 'A',
428
+ // Suspends
429
+ 'Suspend! [2]',
430
+ 'Loading...',
431
+ ]);
432
+
433
+ // While we're still waiting for the low-pri update to complete, update the
434
+ // high-pri text at high priority.
435
+ ReactNoop.flushSync(() => {
436
+ ReactNoop.render(<App highPri="B" lowPri="1" />);
437
+ });
438
+ expect(Scheduler).toHaveYielded(['B', '1']);
439
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
440
454
- // While we're still waiting for the low-pri update to complete, update the
455
- // high-pri text at high priority.
456
- ReactNoop.flushSync(() => {
457
- ReactNoop.render(<App highPri="B" lowPri="1" />);
458
- });
459
- expect(Scheduler).toHaveYielded(['B', '1']);
460
- expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
461
-
462
- // Unblock the low-pri text and finish
463
- await advanceTimers(0);
464
- expect(Scheduler).toHaveYielded(['Promise resolved [2]']);
465
- expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
466
- });
441
+ // Unblock the low-pri text and finish
442
+ await advanceTimers(0);
443
+ expect(Scheduler).toHaveYielded(['Promise resolved [2]']);
444
+ expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]);
445
+ });
446
468
- it('keeps working on lower priority work after being pinged', async () => {
469
- // Advance the virtual time so that we're close to the edge of a bucket.
470
- ReactNoop.expire(149);
447
+ it('keeps working on lower priority work after being pinged', async () => {
448
+ // Advance the virtual time so that we're close to the edge of a bucket.
449
+ ReactNoop.expire(149);
450
472
- function App(props) {
473
- return (
474
- <Suspense fallback={<Text text="Loading..." />}>
475
- {props.showA && <AsyncText text="A" />}
476
- {props.showB && <Text text="B" />}
477
- </Suspense>
478
- );
479
- }
451
+ function App(props) {
452
+ return (
453
+ <Suspense fallback={<Text text="Loading..." />}>
454
+ {props.showA && <AsyncText text="A" />}
455
+ {props.showB && <Text text="B" />}
456
+ </Suspense>
457
+ );
458
+ }
459
+
460
+ ReactNoop.render(<App showA={false} showB={false} />);
461
+ expect(Scheduler).toFlushAndYield([]);
462
+ expect(ReactNoop.getChildren()).toEqual([]);
463
+
464
+ ReactNoop.render(<App showA={true} showB={false} />);
465
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
466
+ expect(ReactNoop.getChildren()).toEqual([]);
467
+
468
+ // Advance React's virtual time by enough to fall into a new async bucket,
469
+ // but not enough to expire the suspense timeout.
470
+ ReactNoop.expire(120);
471
+ ReactNoop.render(<App showA={true} showB={true} />);
472
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'B', 'Loading...']);
473
+ expect(ReactNoop.getChildren()).toEqual([]);
474
+
475
+ await advanceTimers(0);
476
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
477
+ expect(Scheduler).toFlushAndYield(['A', 'B']);
478
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
479
+ });
480
481
- ReactNoop.render(<App showA={false} showB={false} />);
482
- expect(Scheduler).toFlushAndYield([]);
483
- expect(ReactNoop.getChildren()).toEqual([]);
481
+ it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => {
482
+ function App(props) {
483
+ if (props.hide) {
484
+ return <Text text="(empty)" />;
485
+ }
486
+ return (
487
+ <Suspense fallback="Loading...">
488
+ <AsyncText ms={2000} text="Async" />
489
+ </Suspense>
490
+ );
491
+ }
492
485
- ReactNoop.render(<App showA={true} showB={false} />);
486
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
487
- expect(ReactNoop.getChildren()).toEqual([]);
488
-
489
- // Advance React's virtual time by enough to fall into a new async bucket,
490
- // but not enough to expire the suspense timeout.
491
- ReactNoop.expire(120);
492
- ReactNoop.render(<App showA={true} showB={true} />);
493
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'B', 'Loading...']);
494
- expect(ReactNoop.getChildren()).toEqual([]);
495
-
496
- await advanceTimers(0);
497
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
498
- expect(Scheduler).toFlushAndYield(['A', 'B']);
499
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
500
- });
493
+ // Schedule a high pri update and a low pri update, without rendering in
494
+ // between.
495
+ ReactNoop.discreteUpdates(() => {
496
+ // High pri
497
+ ReactNoop.render(<App />);
498
+ });
499
+ // Low pri
500
+ ReactNoop.render(<App hide={true} />);
501
+
502
+ expect(Scheduler).toFlushAndYield([
503
+ // The first update suspends
504
+ 'Suspend! [Async]',
505
+ // but we have another pending update that we can work on
506
+ '(empty)',
507
+ ]);
508
+ expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
509
+ });
510
502
- it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => {
503
- function App(props) {
504
- if (props.hide) {
505
- return <Text text="(empty)" />;
506
- }
507
- return (
508
- <Suspense fallback="Loading...">
509
- <AsyncText ms={2000} text="Async" />
510
- </Suspense>
511
- );
512
- }
511
+ it('tries each subsequent level after suspending', async () => {
512
+ const root = ReactNoop.createRoot();
513
+
514
+ function App({step, shouldSuspend}) {
515
+ return (
516
+ <Suspense fallback="Loading...">
517
+ <Text text="Sibling" />
518
+ {shouldSuspend ? (
519
+ <AsyncText ms={10000} text={'Step ' + step} />
520
+ ) : (
521
+ <Text text={'Step ' + step} />
522
+ )}
523
+ </Suspense>
524
+ );
525
+ }
526
514
- // Schedule a high pri update and a low pri update, without rendering in
515
- // between.
516
- ReactNoop.discreteUpdates(() => {
517
- // High pri
518
- ReactNoop.render(<App />);
519
- });
520
- // Low pri
521
- ReactNoop.render(<App hide={true} />);
522
-
523
- expect(Scheduler).toFlushAndYield([
524
- // The first update suspends
525
- 'Suspend! [Async]',
526
- // but we have another pending update that we can work on
527
- '(empty)',
528
- ]);
529
- expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
527
+ function interrupt() {
528
+ // React has a heuristic to batch all updates that occur within the same
529
+ // event. This is a trick to circumvent that heuristic.
530
+ ReactNoop.flushSync(() => {
531
+ ReactNoop.renderToRootWithID(null, 'other-root');
532
});
533
+ }
534
532
- it('tries each subsequent level after suspending', async () => {
533
- const root = ReactNoop.createRoot();
534
-
535
- function App({step, shouldSuspend}) {
536
- return (
537
- <Suspense fallback="Loading...">
538
- <Text text="Sibling" />
539
- {shouldSuspend ? (
540
- <AsyncText ms={10000} text={'Step ' + step} />
541
- ) : (
542
- <Text text={'Step ' + step} />
543
- )}
544
- </Suspense>
545
- );
546
- }
547
-
548
- function interrupt() {
549
- // React has a heuristic to batch all updates that occur within the same
550
- // event. This is a trick to circumvent that heuristic.
551
- ReactNoop.flushSync(() => {
552
- ReactNoop.renderToRootWithID(null, 'other-root');
553
- });
554
- }
555
-
556
- // Mount the Suspense boundary without suspending, so that the subsequent
557
- // updates suspend with a delay.
558
- await ReactNoop.act(async () => {
559
- root.render(<App step={0} shouldSuspend={false} />);
560
- });
561
- await advanceTimers(1000);
562
- expect(Scheduler).toHaveYielded(['Sibling', 'Step 0']);
563
-
564
- // Schedule an update at several distinct expiration times
565
- await ReactNoop.act(async () => {
566
- root.render(<App step={1} shouldSuspend={true} />);
567
- Scheduler.unstable_advanceTime(1000);
568
- expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
569
- interrupt();
570
-
571
- root.render(<App step={2} shouldSuspend={true} />);
572
- Scheduler.unstable_advanceTime(1000);
573
- expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
574
- interrupt();
535
+ // Mount the Suspense boundary without suspending, so that the subsequent
536
+ // updates suspend with a delay.
537
+ await ReactNoop.act(async () => {
538
+ root.render(<App step={0} shouldSuspend={false} />);
539
+ });
540
+ await advanceTimers(1000);
541
+ expect(Scheduler).toHaveYielded(['Sibling', 'Step 0']);
542
+
543
+ // Schedule an update at several distinct expiration times
544
+ await ReactNoop.act(async () => {
545
+ root.render(<App step={1} shouldSuspend={true} />);
546
+ Scheduler.unstable_advanceTime(1000);
547
+ expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
548
+ interrupt();
549
+
550
+ root.render(<App step={2} shouldSuspend={true} />);
551
+ Scheduler.unstable_advanceTime(1000);
552
+ expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
553
+ interrupt();
554
+
555
+ root.render(<App step={3} shouldSuspend={true} />);
556
+ Scheduler.unstable_advanceTime(1000);
557
+ expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
558
+ interrupt();
559
+
560
+ root.render(<App step={4} shouldSuspend={false} />);
561
+ });
562
576
- root.render(<App step={3} shouldSuspend={true} />);
577
- Scheduler.unstable_advanceTime(1000);
578
- expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
579
- interrupt();
563
+ // Should suspend at each distinct level
564
+ expect(Scheduler).toHaveYielded([
565
+ 'Sibling',
566
+ 'Suspend! [Step 1]',
567
+ 'Sibling',
568
+ 'Suspend! [Step 2]',
569
+ 'Sibling',
570
+ 'Suspend! [Step 3]',
571
+ 'Sibling',
572
+ 'Step 4',
573
+ ]);
574
+ });
575
581
- root.render(<App step={4} shouldSuspend={false} />);
582
- });
576
+ it('forces an expiration after an update times out', async () => {
577
+ ReactNoop.render(
578
+ <Fragment>
579
+ <Suspense fallback={<Text text="Loading..." />} />
580
+ </Fragment>,
581
+ );
582
+ expect(Scheduler).toFlushAndYield([]);
583
+
584
+ ReactNoop.render(
585
+ <Fragment>
586
+ <Suspense fallback={<Text text="Loading..." />}>
587
+ <AsyncText text="Async" ms={20000} />
588
+ </Suspense>
589
+ <Text text="Sync" />
590
+ </Fragment>,
591
+ );
592
+
593
+ expect(Scheduler).toFlushAndYield([
594
+ // The async child suspends
595
+ 'Suspend! [Async]',
596
+ // Render the placeholder
597
+ 'Loading...',
598
+ // Continue on the sibling
599
+ 'Sync',
600
+ ]);
601
+ // The update hasn't expired yet, so we commit nothing.
602
+ expect(ReactNoop.getChildren()).toEqual([]);
603
+
604
+ // Advance both React's virtual time and Jest's timers by enough to expire
605
+ // the update, but not by enough to flush the suspending promise.
606
+ ReactNoop.expire(10000);
607
+ await advanceTimers(10000);
608
+ // No additional rendering work is required, since we already prepared
609
+ // the placeholder.
610
+ expect(Scheduler).toHaveYielded([]);
611
+ // Should have committed the placeholder.
612
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
613
+
614
+ // Once the promise resolves, we render the suspended view
615
+ await advanceTimers(10000);
616
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
617
+ expect(Scheduler).toFlushAndYield(['Async']);
618
+ expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
619
+ });
620
584
- // Should suspend at each distinct level
585
- expect(Scheduler).toHaveYielded([
586
- 'Sibling',
587
- 'Suspend! [Step 1]',
588
- 'Sibling',
589
- 'Suspend! [Step 2]',
590
- 'Sibling',
591
- 'Suspend! [Step 3]',
592
- 'Sibling',
593
- 'Step 4',
594
- ]);
595
- });
621
+ it('switches to an inner fallback after suspending for a while', async () => {
622
+ // Advance the virtual time so that we're closer to the edge of a bucket.
623
+ ReactNoop.expire(200);
624
+
625
+ ReactNoop.render(
626
+ <Fragment>
627
+ <Text text="Sync" />
628
+ <Suspense fallback={<Text text="Loading outer..." />}>
629
+ <AsyncText text="Outer content" ms={300} />
630
+ <Suspense fallback={<Text text="Loading inner..." />}>
631
+ <AsyncText text="Inner content" ms={1000} />
632
+ </Suspense>
633
+ </Suspense>
634
+ </Fragment>,
635
+ );
636
+
637
+ expect(Scheduler).toFlushAndYield([
638
+ 'Sync',
639
+ // The async content suspends
640
+ 'Suspend! [Outer content]',
641
+ 'Suspend! [Inner content]',
642
+ 'Loading inner...',
643
+ 'Loading outer...',
644
+ ]);
645
+ // The outer loading state finishes immediately.
646
+ expect(ReactNoop.getChildren()).toEqual([
647
+ span('Sync'),
648
+ span('Loading outer...'),
649
+ ]);
650
+
651
+ // Resolve the outer promise.
652
+ ReactNoop.expire(300);
653
+ await advanceTimers(300);
654
+ expect(Scheduler).toHaveYielded(['Promise resolved [Outer content]']);
655
+ expect(Scheduler).toFlushAndYield([
656
+ 'Outer content',
657
+ 'Suspend! [Inner content]',
658
+ 'Loading inner...',
659
+ ]);
660
+ // Don't commit the inner placeholder yet.
661
+ expect(ReactNoop.getChildren()).toEqual([
662
+ span('Sync'),
663
+ span('Loading outer...'),
664
+ ]);
665
+
666
+ // Expire the inner timeout.
667
+ ReactNoop.expire(500);
668
+ await advanceTimers(500);
669
+ // Now that 750ms have elapsed since the outer placeholder timed out,
670
+ // we can timeout the inner placeholder.
671
+ expect(ReactNoop.getChildren()).toEqual([
672
+ span('Sync'),
673
+ span('Outer content'),
674
+ span('Loading inner...'),
675
+ ]);
676
+
677
+ // Finally, flush the inner promise. We should see the complete screen.
678
+ ReactNoop.expire(1000);
679
+ await advanceTimers(1000);
680
+ expect(Scheduler).toHaveYielded(['Promise resolved [Inner content]']);
681
+ expect(Scheduler).toFlushAndYield(['Inner content']);
682
+ expect(ReactNoop.getChildren()).toEqual([
683
+ span('Sync'),
684
+ span('Outer content'),
685
+ span('Inner content'),
686
+ ]);
687
+ });
688
597
- it('forces an expiration after an update times out', async () => {
598
- ReactNoop.render(
599
- <Fragment>
600
- <Suspense fallback={<Text text="Loading..." />} />
601
- </Fragment>,
602
- );
603
- expect(Scheduler).toFlushAndYield([]);
689
+ it('renders an expiration boundary synchronously', async () => {
690
+ spyOnDev(console, 'error');
691
+ // Synchronously render a tree that suspends
692
+ ReactNoop.flushSync(() =>
693
+ ReactNoop.render(
694
+ <Fragment>
695
+ <Suspense fallback={<Text text="Loading..." />}>
696
+ <AsyncText text="Async" />
697
+ </Suspense>
698
+ <Text text="Sync" />
699
+ </Fragment>,
700
+ ),
701
+ );
702
+ expect(Scheduler).toHaveYielded([
703
+ // The async child suspends
704
+ 'Suspend! [Async]',
705
+ // We immediately render the fallback UI
706
+ 'Loading...',
707
+ // Continue on the sibling
708
+ 'Sync',
709
+ ]);
710
+ // The tree commits synchronously
711
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
712
+
713
+ // Once the promise resolves, we render the suspended view
714
+ await advanceTimers(0);
715
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
716
+ expect(Scheduler).toFlushAndYield(['Async']);
717
+ expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
718
+ });
719
605
- ReactNoop.render(
606
- <Fragment>
607
- <Suspense fallback={<Text text="Loading..." />}>
608
- <AsyncText text="Async" ms={20000} />
720
+ it('suspending inside an expired expiration boundary will bubble to the next one', async () => {
721
+ ReactNoop.flushSync(() =>
722
+ ReactNoop.render(
723
+ <Fragment>
724
+ <Suspense fallback={<Text text="Loading (outer)..." />}>
725
+ <Suspense fallback={<AsyncText text="Loading (inner)..." />}>
726
+ <AsyncText text="Async" />
727
</Suspense>
728
<Text text="Sync" />
611
- </Fragment>,
612
- );
613
-
614
- expect(Scheduler).toFlushAndYield([
615
- // The async child suspends
616
- 'Suspend! [Async]',
617
- // Render the placeholder
618
- 'Loading...',
619
- // Continue on the sibling
620
- 'Sync',
621
- ]);
622
- // The update hasn't expired yet, so we commit nothing.
623
- expect(ReactNoop.getChildren()).toEqual([]);
624
-
625
- // Advance both React's virtual time and Jest's timers by enough to expire
626
- // the update, but not by enough to flush the suspending promise.
627
- ReactNoop.expire(10000);
628
- await advanceTimers(10000);
629
- // No additional rendering work is required, since we already prepared
630
- // the placeholder.
631
- expect(Scheduler).toHaveYielded([]);
632
- // Should have committed the placeholder.
633
- expect(ReactNoop.getChildren()).toEqual([
634
- span('Loading...'),
635
- span('Sync'),
636
- ]);
637
-
638
- // Once the promise resolves, we render the suspended view
639
- await advanceTimers(10000);
640
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
641
- expect(Scheduler).toFlushAndYield(['Async']);
642
- expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
643
- });
644
-
645
- it('switches to an inner fallback after suspending for a while', async () => {
646
- // Advance the virtual time so that we're closer to the edge of a bucket.
647
- ReactNoop.expire(200);
648
-
649
- ReactNoop.render(
650
- <Fragment>
651
- <Text text="Sync" />
652
- <Suspense fallback={<Text text="Loading outer..." />}>
653
- <AsyncText text="Outer content" ms={300} />
654
- <Suspense fallback={<Text text="Loading inner..." />}>
655
- <AsyncText text="Inner content" ms={1000} />
656
- </Suspense>
657
- </Suspense>
658
- </Fragment>,
659
- );
660
-
661
- expect(Scheduler).toFlushAndYield([
662
- 'Sync',
663
- // The async content suspends
664
- 'Suspend! [Outer content]',
665
- 'Suspend! [Inner content]',
666
- 'Loading inner...',
667
- 'Loading outer...',
668
- ]);
669
- // The outer loading state finishes immediately.
670
- expect(ReactNoop.getChildren()).toEqual([
671
- span('Sync'),
672
- span('Loading outer...'),
673
- ]);
674
-
675
- // Resolve the outer promise.
676
- ReactNoop.expire(300);
677
- await advanceTimers(300);
678
- expect(Scheduler).toHaveYielded(['Promise resolved [Outer content]']);
679
- expect(Scheduler).toFlushAndYield([
680
- 'Outer content',
681
- 'Suspend! [Inner content]',
682
- 'Loading inner...',
683
- ]);
684
- // Don't commit the inner placeholder yet.
685
- expect(ReactNoop.getChildren()).toEqual([
686
- span('Sync'),
687
- span('Loading outer...'),
688
- ]);
689
-
690
- // Expire the inner timeout.
691
- ReactNoop.expire(500);
692
- await advanceTimers(500);
693
- // Now that 750ms have elapsed since the outer placeholder timed out,
694
- // we can timeout the inner placeholder.
695
- expect(ReactNoop.getChildren()).toEqual([
696
- span('Sync'),
697
- span('Outer content'),
698
- span('Loading inner...'),
699
- ]);
700
-
701
- // Finally, flush the inner promise. We should see the complete screen.
702
- ReactNoop.expire(1000);
703
- await advanceTimers(1000);
704
- expect(Scheduler).toHaveYielded(['Promise resolved [Inner content]']);
705
- expect(Scheduler).toFlushAndYield(['Inner content']);
706
- expect(ReactNoop.getChildren()).toEqual([
707
- span('Sync'),
708
- span('Outer content'),
709
- span('Inner content'),
710
- ]);
711
- });
712
-
713
- it('renders an expiration boundary synchronously', async () => {
714
- spyOnDev(console, 'error');
715
- // Synchronously render a tree that suspends
716
- ReactNoop.flushSync(() =>
717
- ReactNoop.render(
718
- <Fragment>
719
- <Suspense fallback={<Text text="Loading..." />}>
720
- <AsyncText text="Async" />
721
- </Suspense>
722
- <Text text="Sync" />
723
- </Fragment>,
724
- ),
725
- );
726
- expect(Scheduler).toHaveYielded([
727
- // The async child suspends
728
- 'Suspend! [Async]',
729
- // We immediately render the fallback UI
730
- 'Loading...',
731
- // Continue on the sibling
732
- 'Sync',
733
- ]);
734
- // The tree commits synchronously
735
- expect(ReactNoop.getChildren()).toEqual([
736
- span('Loading...'),
737
- span('Sync'),
738
- ]);
739
-
740
- // Once the promise resolves, we render the suspended view
741
- await advanceTimers(0);
742
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
743
- expect(Scheduler).toFlushAndYield(['Async']);
744
- expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
745
- });
746
-
747
- it('suspending inside an expired expiration boundary will bubble to the next one', async () => {
748
- ReactNoop.flushSync(() =>
749
- ReactNoop.render(
750
- <Fragment>
751
- <Suspense fallback={<Text text="Loading (outer)..." />}>
752
- <Suspense fallback={<AsyncText text="Loading (inner)..." />}>
753
- <AsyncText text="Async" />
754
- </Suspense>
755
- <Text text="Sync" />
756
- </Suspense>
757
- </Fragment>,
758
- ),
759
- );
760
- expect(Scheduler).toHaveYielded([
761
- 'Suspend! [Async]',
762
- 'Suspend! [Loading (inner)...]',
763
- 'Sync',
764
- 'Loading (outer)...',
765
- ]);
766
- // The tree commits synchronously
767
- expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]);
768
- });
729
+ </Suspense>
730
+ </Fragment>,
731
+ ),
732
+ );
733
+ expect(Scheduler).toHaveYielded([
734
+ 'Suspend! [Async]',
735
+ 'Suspend! [Loading (inner)...]',
736
+ 'Sync',
737
+ 'Loading (outer)...',
738
+ ]);
739
+ // The tree commits synchronously
740
+ expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]);
741
+ });
742
770
- it('expires early by default', async () => {
771
- ReactNoop.render(
772
- <Fragment>
773
- <Suspense fallback={<Text text="Loading..." />} />
774
- </Fragment>,
775
- );
776
- expect(Scheduler).toFlushAndYield([]);
743
+ it('expires early by default', async () => {
744
+ ReactNoop.render(
745
+ <Fragment>
746
+ <Suspense fallback={<Text text="Loading..." />} />
747
+ </Fragment>,
748
+ );
749
+ expect(Scheduler).toFlushAndYield([]);
750
+
751
+ ReactNoop.render(
752
+ <Fragment>
753
+ <Suspense fallback={<Text text="Loading..." />}>
754
+ <AsyncText text="Async" ms={3000} />
755
+ </Suspense>
756
+ <Text text="Sync" />
757
+ </Fragment>,
758
+ );
759
+
760
+ expect(Scheduler).toFlushAndYield([
761
+ // The async child suspends
762
+ 'Suspend! [Async]',
763
+ 'Loading...',
764
+ // Continue on the sibling
765
+ 'Sync',
766
+ ]);
767
+ // The update hasn't expired yet, so we commit nothing.
768
+ expect(ReactNoop.getChildren()).toEqual([]);
769
+
770
+ // Advance both React's virtual time and Jest's timers by enough to trigger
771
+ // the timeout, but not by enough to flush the promise or reach the true
772
+ // expiration time.
773
+ ReactNoop.expire(2000);
774
+ await advanceTimers(2000);
775
+ expect(Scheduler).toFlushWithoutYielding();
776
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]);
777
+
778
+ // Once the promise resolves, we render the suspended view
779
+ await advanceTimers(1000);
780
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
781
+ expect(Scheduler).toFlushAndYield(['Async']);
782
+ expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
783
+ });
784
778
- ReactNoop.render(
779
- <Fragment>
780
- <Suspense fallback={<Text text="Loading..." />}>
781
- <AsyncText text="Async" ms={3000} />
782
- </Suspense>
783
- <Text text="Sync" />
784
- </Fragment>,
785
- );
785
+ it('resolves successfully even if fallback render is pending', async () => {
786
+ ReactNoop.render(
787
+ <>
788
+ <Suspense fallback={<Text text="Loading..." />} />
789
+ </>,
790
+ );
791
+ expect(Scheduler).toFlushAndYield([]);
792
+ expect(ReactNoop.getChildren()).toEqual([]);
793
+ ReactNoop.render(
794
+ <>
795
+ <Suspense fallback={<Text text="Loading..." />}>
796
+ <AsyncText text="Async" ms={3000} />
797
+ </Suspense>
798
+ </>,
799
+ );
800
+ expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
801
+ await advanceTimers(1500);
802
+ expect(Scheduler).toHaveYielded([]);
803
+ expect(ReactNoop.getChildren()).toEqual([]);
804
+ // Before we have a chance to flush, the promise resolves.
805
+ await advanceTimers(2000);
806
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
807
+ expect(Scheduler).toFlushAndYield([
808
+ // We've now pinged the boundary but we don't know if we should restart yet,
809
+ // because we haven't completed the suspense boundary.
810
+ 'Loading...',
811
+ // Once we've completed the boundary we restarted.
812
+ 'Async',
813
+ ]);
814
+ expect(ReactNoop.getChildren()).toEqual([span('Async')]);
815
+ });
816
787
- expect(Scheduler).toFlushAndYield([
788
- // The async child suspends
789
- 'Suspend! [Async]',
790
- 'Loading...',
791
- // Continue on the sibling
792
- 'Sync',
793
- ]);
794
- // The update hasn't expired yet, so we commit nothing.
795
- expect(ReactNoop.getChildren()).toEqual([]);
796
-
797
- // Advance both React's virtual time and Jest's timers by enough to trigger
798
- // the timeout, but not by enough to flush the promise or reach the true
799
- // expiration time.
800
- ReactNoop.expire(2000);
801
- await advanceTimers(2000);
802
- expect(Scheduler).toFlushWithoutYielding();
803
- expect(ReactNoop.getChildren()).toEqual([
804
- span('Loading...'),
805
- span('Sync'),
806
- ]);
817
+ it('throws a helpful error when an update is suspends without a placeholder', () => {
818
+ ReactNoop.render(<AsyncText ms={1000} text="Async" />);
819
+ expect(Scheduler).toFlushAndThrow(
820
+ 'AsyncText suspended while rendering, but no fallback UI was specified.',
821
+ );
822
+ });
823
808
- // Once the promise resolves, we render the suspended view
809
- await advanceTimers(1000);
810
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
811
- expect(Scheduler).toFlushAndYield(['Async']);
812
- expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]);
813
- });
824
+ it('a Suspense component correctly handles more than one suspended child', async () => {
825
+ ReactNoop.render(
826
+ <Suspense fallback={<Text text="Loading..." />}>
827
+ <AsyncText text="A" ms={100} />
828
+ <AsyncText text="B" ms={100} />
829
+ </Suspense>,
830
+ );
831
+ Scheduler.unstable_advanceTime(10000);
832
+ expect(Scheduler).toFlushExpired([
833
+ 'Suspend! [A]',
834
+ 'Suspend! [B]',
835
+ 'Loading...',
836
+ ]);
837
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
838
+
839
+ await advanceTimers(100);
840
+
841
+ expect(Scheduler).toHaveYielded([
842
+ 'Promise resolved [A]',
843
+ 'Promise resolved [B]',
844
+ ]);
845
+ expect(Scheduler).toFlushAndYield(['A', 'B']);
846
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
847
+ });
848
815
- it('resolves successfully even if fallback render is pending', async () => {
816
- ReactNoop.render(
817
- <>
818
- <Suspense fallback={<Text text="Loading..." />} />
819
- </>,
820
- );
821
- expect(Scheduler).toFlushAndYield([]);
822
- expect(ReactNoop.getChildren()).toEqual([]);
823
- ReactNoop.render(
824
- <>
825
- <Suspense fallback={<Text text="Loading..." />}>
826
- <AsyncText text="Async" ms={3000} />
827
- </Suspense>
828
- </>,
829
- );
830
- expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']);
831
- await advanceTimers(1500);
832
- expect(Scheduler).toHaveYielded([]);
833
- expect(ReactNoop.getChildren()).toEqual([]);
834
- // Before we have a chance to flush, the promise resolves.
835
- await advanceTimers(2000);
836
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
837
- expect(Scheduler).toFlushAndYield([
838
- // We've now pinged the boundary but we don't know if we should restart yet,
839
- // because we haven't completed the suspense boundary.
840
- 'Loading...',
841
- // Once we've completed the boundary we restarted.
842
- 'Async',
843
- ]);
844
- expect(ReactNoop.getChildren()).toEqual([span('Async')]);
845
- });
849
+ it('can resume rendering earlier than a timeout', async () => {
850
+ ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
851
+ expect(Scheduler).toFlushAndYield([]);
852
+
853
+ ReactNoop.render(
854
+ <Suspense fallback={<Text text="Loading..." />}>
855
+ <AsyncText text="Async" ms={100} />
856
+ </Suspense>,
857
+ );
858
+ expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
859
+ expect(ReactNoop.getChildren()).toEqual([]);
860
+
861
+ // Advance time by an amount slightly smaller than what's necessary to
862
+ // resolve the promise
863
+ await advanceTimers(99);
864
+
865
+ // Nothing has rendered yet
866
+ expect(Scheduler).toFlushWithoutYielding();
867
+ expect(ReactNoop.getChildren()).toEqual([]);
868
+
869
+ // Resolve the promise
870
+ await advanceTimers(1);
871
+ // We can now resume rendering
872
+ expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
873
+ expect(Scheduler).toFlushAndYield(['Async']);
874
+ expect(ReactNoop.getChildren()).toEqual([span('Async')]);
875
+ });
876
847
- it('throws a helpful error when an update is suspends without a placeholder', () => {
848
- ReactNoop.render(<AsyncText ms={1000} text="Async" />);
849
- expect(Scheduler).toFlushAndThrow(
850
- 'AsyncText suspended while rendering, but no fallback UI was specified.',
851
- );
852
- });
877
+ it('starts working on an update even if its priority falls between two suspended levels', async () => {
878
+ function App(props) {
879
+ return (
880
+ <Suspense fallback={<Text text="Loading..." />}>
881
+ {props.text === 'C' || props.text === 'S' ? (
882
+ <Text text={props.text} />
883
+ ) : (
884
+ <AsyncText text={props.text} ms={10000} />
885
+ )}
886
+ </Suspense>
887
+ );
888
+ }
889
+
890
+ // First mount without suspending. This ensures we already have content
891
+ // showing so that subsequent updates will suspend.
892
+ ReactNoop.render(<App text="S" />);
893
+ expect(Scheduler).toFlushAndYield(['S']);
894
+
895
+ // Schedule an update, and suspend for up to 5 seconds.
896
+ React.unstable_withSuspenseConfig(
897
+ () => ReactNoop.render(<App text="A" />),
898
+ {
899
+ timeoutMs: 5000,
900
+ },
901
+ );
902
+ // The update should suspend.
903
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
904
+ expect(ReactNoop.getChildren()).toEqual([span('S')]);
905
+
906
+ // Advance time until right before it expires.
907
+ await advanceTimers(4999);
908
+ ReactNoop.expire(4999);
909
+ expect(Scheduler).toFlushWithoutYielding();
910
+ expect(ReactNoop.getChildren()).toEqual([span('S')]);
911
+
912
+ // Schedule another low priority update.
913
+ React.unstable_withSuspenseConfig(
914
+ () => ReactNoop.render(<App text="B" />),
915
+ {
916
+ timeoutMs: 10000,
917
+ },
918
+ );
919
+ // This update should also suspend.
920
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
921
+ expect(ReactNoop.getChildren()).toEqual([span('S')]);
922
+
923
+ // Schedule a regular update. Its expiration time will fall between
924
+ // the expiration times of the previous two updates.
925
+ ReactNoop.render(<App text="C" />);
926
+ expect(Scheduler).toFlushAndYield(['C']);
927
+ expect(ReactNoop.getChildren()).toEqual([span('C')]);
928
+
929
+ await advanceTimers(10000);
930
+ // Flush the remaining work.
931
+ expect(Scheduler).toHaveYielded([
932
+ 'Promise resolved [A]',
933
+ 'Promise resolved [B]',
934
+ ]);
935
+ // Nothing else to render.
936
+ expect(Scheduler).toFlushWithoutYielding();
937
+ expect(ReactNoop.getChildren()).toEqual([span('C')]);
938
+ });
939
854
- it('a Suspense component correctly handles more than one suspended child', async () => {
855
- ReactNoop.render(
940
+ it('flushes all expired updates in a single batch', async () => {
941
+ class Foo extends React.Component {
942
+ componentDidUpdate() {
943
+ Scheduler.unstable_yieldValue('Commit: ' + this.props.text);
944
+ }
945
+ componentDidMount() {
946
+ Scheduler.unstable_yieldValue('Commit: ' + this.props.text);
947
+ }
948
+ render() {
949
+ return (
950
<Suspense fallback={<Text text="Loading..." />}>
857
- <AsyncText text="A" ms={100} />
858
- <AsyncText text="B" ms={100} />
859
- </Suspense>,
951
+ <AsyncText ms={20000} text={this.props.text} />
952
+ </Suspense>
953
);
861
- Scheduler.unstable_advanceTime(10000);
862
- expect(Scheduler).toFlushExpired([
863
- 'Suspend! [A]',
864
- 'Suspend! [B]',
865
- 'Loading...',
866
- ]);
867
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
868
-
869
- await advanceTimers(100);
870
-
871
- expect(Scheduler).toHaveYielded([
872
- 'Promise resolved [A]',
873
- 'Promise resolved [B]',
874
- ]);
875
- expect(Scheduler).toFlushAndYield(['A', 'B']);
876
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
877
- });
954
+ }
955
+ }
956
+
957
+ ReactNoop.render(<Foo text="" />);
958
+ ReactNoop.expire(1000);
959
+ jest.advanceTimersByTime(1000);
960
+ ReactNoop.render(<Foo text="go" />);
961
+ ReactNoop.expire(1000);
962
+ jest.advanceTimersByTime(1000);
963
+ ReactNoop.render(<Foo text="good" />);
964
+ ReactNoop.expire(1000);
965
+ jest.advanceTimersByTime(1000);
966
+ ReactNoop.render(<Foo text="goodbye" />);
967
+
968
+ Scheduler.unstable_advanceTime(10000);
969
+ jest.advanceTimersByTime(10000);
970
+
971
+ expect(Scheduler).toFlushExpired([
972
+ 'Suspend! [goodbye]',
973
+ 'Loading...',
974
+ 'Commit: goodbye',
975
+ ]);
976
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
977
+
978
+ Scheduler.unstable_advanceTime(20000);
979
+ await advanceTimers(20000);
980
+ expect(Scheduler).toHaveYielded(['Promise resolved [goodbye]']);
981
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
982
+
983
+ expect(Scheduler).toFlushAndYield(['goodbye']);
984
+ expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
985
+ });
986
879
- it('can resume rendering earlier than a timeout', async () => {
880
- ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
881
- expect(Scheduler).toFlushAndYield([]);
987
+ it('a suspended update that expires', async () => {
988
+ // Regression test. This test used to fall into an infinite loop.
989
+ function ExpensiveText({text}) {
990
+ // This causes the update to expire.
991
+ Scheduler.unstable_advanceTime(10000);
992
+ // Then something suspends.
993
+ return <AsyncText text={text} ms={200000} />;
994
+ }
995
+
996
+ function App() {
997
+ return (
998
+ <Suspense fallback="Loading...">
999
+ <ExpensiveText text="A" />
1000
+ <ExpensiveText text="B" />
1001
+ <ExpensiveText text="C" />
1002
+ </Suspense>
1003
+ );
1004
+ }
1005
+
1006
+ ReactNoop.render(<App />);
1007
+ expect(Scheduler).toFlushAndYield([
1008
+ 'Suspend! [A]',
1009
+ 'Suspend! [B]',
1010
+ 'Suspend! [C]',
1011
+ ]);
1012
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
1013
+
1014
+ await advanceTimers(200000);
1015
+ expect(Scheduler).toHaveYielded([
1016
+ 'Promise resolved [A]',
1017
+ 'Promise resolved [B]',
1018
+ 'Promise resolved [C]',
1019
+ ]);
1020
+
1021
+ expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
1022
+ expect(ReactNoop).toMatchRenderedOutput(
1023
+ <>
1024
+ <span prop="A" />
1025
+ <span prop="B" />
1026
+ <span prop="C" />
1027
+ </>,
1028
+ );
1029
+ });
1030
883
- ReactNoop.render(
1031
+ describe('legacy mode mode', () => {
1032
+ it('times out immediately', async () => {
1033
+ function App() {
1034
+ return (
1035
<Suspense fallback={<Text text="Loading..." />}>
885
- <AsyncText text="Async" ms={100} />
886
- </Suspense>,
887
- );
888
- expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']);
889
- expect(ReactNoop.getChildren()).toEqual([]);
890
-
891
- // Advance time by an amount slightly smaller than what's necessary to
892
- // resolve the promise
893
- await advanceTimers(99);
894
-
895
- // Nothing has rendered yet
896
- expect(Scheduler).toFlushWithoutYielding();
897
- expect(ReactNoop.getChildren()).toEqual([]);
898
-
899
- // Resolve the promise
900
- await advanceTimers(1);
901
- // We can now resume rendering
902
- expect(Scheduler).toHaveYielded(['Promise resolved [Async]']);
903
- expect(Scheduler).toFlushAndYield(['Async']);
904
- expect(ReactNoop.getChildren()).toEqual([span('Async')]);
905
- });
906
-
907
- it('starts working on an update even if its priority falls between two suspended levels', async () => {
908
- function App(props) {
909
- return (
910
- <Suspense fallback={<Text text="Loading..." />}>
911
- {props.text === 'C' || props.text === 'S' ? (
912
- <Text text={props.text} />
913
- ) : (
914
- <AsyncText text={props.text} ms={10000} />
915
- )}
916
- </Suspense>
917
- );
918
- }
919
-
920
- // First mount without suspending. This ensures we already have content
921
- // showing so that subsequent updates will suspend.
922
- ReactNoop.render(<App text="S" />);
923
- expect(Scheduler).toFlushAndYield(['S']);
924
-
925
- // Schedule an update, and suspend for up to 5 seconds.
926
- React.unstable_withSuspenseConfig(
927
- () => ReactNoop.render(<App text="A" />),
928
- {
929
- timeoutMs: 5000,
930
- },
931
- );
932
- // The update should suspend.
933
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
934
- expect(ReactNoop.getChildren()).toEqual([span('S')]);
935
-
936
- // Advance time until right before it expires.
937
- await advanceTimers(4999);
938
- ReactNoop.expire(4999);
939
- expect(Scheduler).toFlushWithoutYielding();
940
- expect(ReactNoop.getChildren()).toEqual([span('S')]);
941
-
942
- // Schedule another low priority update.
943
- React.unstable_withSuspenseConfig(
944
- () => ReactNoop.render(<App text="B" />),
945
- {
946
- timeoutMs: 10000,
947
- },
948
- );
949
- // This update should also suspend.
950
- expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
951
- expect(ReactNoop.getChildren()).toEqual([span('S')]);
952
-
953
- // Schedule a regular update. Its expiration time will fall between
954
- // the expiration times of the previous two updates.
955
- ReactNoop.render(<App text="C" />);
956
- expect(Scheduler).toFlushAndYield(['C']);
957
- expect(ReactNoop.getChildren()).toEqual([span('C')]);
958
-
959
- await advanceTimers(10000);
960
- // Flush the remaining work.
961
- expect(Scheduler).toHaveYielded([
962
- 'Promise resolved [A]',
963
- 'Promise resolved [B]',
964
- ]);
965
- // Nothing else to render.
966
- expect(Scheduler).toFlushWithoutYielding();
967
- expect(ReactNoop.getChildren()).toEqual([span('C')]);
968
- });
969
-
970
- it('flushes all expired updates in a single batch', async () => {
971
- class Foo extends React.Component {
972
- componentDidUpdate() {
973
- Scheduler.unstable_yieldValue('Commit: ' + this.props.text);
974
- }
975
- componentDidMount() {
976
- Scheduler.unstable_yieldValue('Commit: ' + this.props.text);
977
- }
978
- render() {
979
- return (
980
- <Suspense fallback={<Text text="Loading..." />}>
981
- <AsyncText ms={20000} text={this.props.text} />
982
- </Suspense>
983
- );
984
- }
985
- }
986
-
987
- ReactNoop.render(<Foo text="" />);
988
- ReactNoop.expire(1000);
989
- jest.advanceTimersByTime(1000);
990
- ReactNoop.render(<Foo text="go" />);
991
- ReactNoop.expire(1000);
992
- jest.advanceTimersByTime(1000);
993
- ReactNoop.render(<Foo text="good" />);
994
- ReactNoop.expire(1000);
995
- jest.advanceTimersByTime(1000);
996
- ReactNoop.render(<Foo text="goodbye" />);
997
-
998
- Scheduler.unstable_advanceTime(10000);
999
- jest.advanceTimersByTime(10000);
1000
-
1001
- expect(Scheduler).toFlushExpired([
1002
- 'Suspend! [goodbye]',
1003
- 'Loading...',
1004
- 'Commit: goodbye',
1005
- ]);
1006
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1007
-
1008
- Scheduler.unstable_advanceTime(20000);
1009
- await advanceTimers(20000);
1010
- expect(Scheduler).toHaveYielded(['Promise resolved [goodbye]']);
1011
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1012
-
1013
- expect(Scheduler).toFlushAndYield(['goodbye']);
1014
- expect(ReactNoop.getChildren()).toEqual([span('goodbye')]);
1015
- });
1016
-
1017
- it('a suspended update that expires', async () => {
1018
- // Regression test. This test used to fall into an infinite loop.
1019
- function ExpensiveText({text}) {
1020
- // This causes the update to expire.
1021
- Scheduler.unstable_advanceTime(10000);
1022
- // Then something suspends.
1023
- return <AsyncText text={text} ms={200000} />;
1024
- }
1025
-
1026
- function App() {
1027
- return (
1028
- <Suspense fallback="Loading...">
1029
- <ExpensiveText text="A" />
1030
- <ExpensiveText text="B" />
1031
- <ExpensiveText text="C" />
1032
- </Suspense>
1033
- );
1034
- }
1035
-
1036
- ReactNoop.render(<App />);
1037
- expect(Scheduler).toFlushAndYield([
1038
- 'Suspend! [A]',
1039
- 'Suspend! [B]',
1040
- 'Suspend! [C]',
1041
- ]);
1042
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
1043
-
1044
- await advanceTimers(200000);
1045
- expect(Scheduler).toHaveYielded([
1046
- 'Promise resolved [A]',
1047
- 'Promise resolved [B]',
1048
- 'Promise resolved [C]',
1049
- ]);
1050
-
1051
- expect(Scheduler).toFlushAndYield(['A', 'B', 'C']);
1052
- expect(ReactNoop).toMatchRenderedOutput(
1053
- <>
1054
- <span prop="A" />
1055
- <span prop="B" />
1056
- <span prop="C" />
1057
- </>,
1036
+ <AsyncText ms={100} text="Result" />
1037
+ </Suspense>
1038
);
1059
- });
1060
-
1061
- describe('legacy mode mode', () => {
1062
- it('times out immediately', async () => {
1063
- function App() {
1064
- return (
1065
- <Suspense fallback={<Text text="Loading..." />}>
1066
- <AsyncText ms={100} text="Result" />
1067
- </Suspense>
1068
- );
1069
- }
1070
-
1071
- // Times out immediately, ignoring the specified threshold.
1072
- ReactNoop.renderLegacySyncRoot(<App />);
1073
- expect(Scheduler).toHaveYielded(['Suspend! [Result]', 'Loading...']);
1074
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1075
-
1076
- ReactNoop.expire(100);
1077
- await advanceTimers(100);
1078
-
1079
- expect(Scheduler).toHaveYielded(['Promise resolved [Result]']);
1080
- expect(Scheduler).toFlushExpired(['Result']);
1081
- expect(ReactNoop.getChildren()).toEqual([span('Result')]);
1082
- });
1083
-
1084
- it('times out immediately when Suspense is in legacy mode', async () => {
1085
- class UpdatingText extends React.Component {
1086
- state = {step: 1};
1087
- render() {
1088
- return <AsyncText ms={100} text={`Step: ${this.state.step}`} />;
1089
- }
1090
- }
1091
-
1092
- function Spinner() {
1093
- return (
1094
- <Fragment>
1095
- <Text text="Loading (1)" />
1096
- <Text text="Loading (2)" />
1097
- <Text text="Loading (3)" />
1098
- </Fragment>
1099
- );
1100
- }
1101
-
1102
- const text = React.createRef(null);
1103
- function App() {
1104
- return (
1105
- <Suspense fallback={<Spinner />}>
1106
- <UpdatingText ref={text} />
1107
- <Text text="Sibling" />
1108
- </Suspense>
1109
- );
1110
- }
1111
-
1112
- // Initial mount.
1113
- ReactNoop.renderLegacySyncRoot(<App />);
1114
- await advanceTimers(100);
1115
- expect(Scheduler).toHaveYielded([
1116
- 'Suspend! [Step: 1]',
1117
- 'Sibling',
1118
- 'Loading (1)',
1119
- 'Loading (2)',
1120
- 'Loading (3)',
1121
- 'Promise resolved [Step: 1]',
1122
- ]);
1123
- expect(Scheduler).toFlushExpired(['Step: 1']);
1124
- expect(ReactNoop).toMatchRenderedOutput(
1125
- <>
1126
- <span prop="Step: 1" />
1127
- <span prop="Sibling" />
1128
- </>,
1129
- );
1130
-
1131
- // Update.
1132
- text.current.setState({step: 2}, () =>
1133
- Scheduler.unstable_yieldValue('Update did commit'),
1134
- );
1135
-
1136
- expect(ReactNoop.flushNextYield()).toEqual([
1137
- 'Suspend! [Step: 2]',
1138
- 'Loading (1)',
1139
- 'Loading (2)',
1140
- 'Loading (3)',
1141
- 'Update did commit',
1142
- ]);
1143
- expect(ReactNoop).toMatchRenderedOutput(
1144
- <>
1145
- <span hidden={true} prop="Step: 1" />
1146
- <span hidden={true} prop="Sibling" />
1147
- <span prop="Loading (1)" />
1148
- <span prop="Loading (2)" />
1149
- <span prop="Loading (3)" />
1150
- </>,
1151
- );
1152
-
1153
- await advanceTimers(100);
1154
- expect(Scheduler).toHaveYielded(['Promise resolved [Step: 2]']);
1155
- expect(Scheduler).toFlushExpired(['Step: 2']);
1156
- expect(ReactNoop).toMatchRenderedOutput(
1157
- <>
1158
- <span prop="Step: 2" />
1159
- <span prop="Sibling" />
1160
- </>,
1161
- );
1162
- });
1163
-
1164
- it('does not re-render siblings in loose mode', async () => {
1165
- class TextWithLifecycle extends React.Component {
1166
- componentDidMount() {
1167
- Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1168
- }
1169
- componentDidUpdate() {
1170
- Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1171
- }
1172
- render() {
1173
- return <Text {...this.props} />;
1174
- }
1175
- }
1176
-
1177
- class AsyncTextWithLifecycle extends React.Component {
1178
- componentDidMount() {
1179
- Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1180
- }
1181
- componentDidUpdate() {
1182
- Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1183
- }
1184
- render() {
1185
- return <AsyncText {...this.props} />;
1186
- }
1187
- }
1188
-
1189
- function App() {
1190
- return (
1191
- <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1192
- <TextWithLifecycle text="A" />
1193
- <AsyncTextWithLifecycle ms={100} text="B" />
1194
- <TextWithLifecycle text="C" />
1195
- </Suspense>
1196
- );
1197
- }
1198
-
1199
- ReactNoop.renderLegacySyncRoot(<App />, () =>
1200
- Scheduler.unstable_yieldValue('Commit root'),
1201
- );
1202
- expect(Scheduler).toHaveYielded([
1203
- 'A',
1204
- 'Suspend! [B]',
1205
- 'C',
1206
-
1207
- 'Loading...',
1208
- 'Mount [A]',
1209
- 'Mount [B]',
1210
- 'Mount [C]',
1211
- // This should be a mount, not an update.
1212
- 'Mount [Loading...]',
1213
- 'Commit root',
1214
- ]);
1215
- expect(ReactNoop).toMatchRenderedOutput(
1216
- <>
1217
- <span hidden={true} prop="A" />
1218
- <span hidden={true} prop="C" />
1219
-
1220
- <span prop="Loading..." />
1221
- </>,
1222
- );
1223
-
1224
- ReactNoop.expire(1000);
1225
- await advanceTimers(1000);
1226
-
1227
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1228
- expect(Scheduler).toFlushExpired(['B']);
1229
- expect(ReactNoop).toMatchRenderedOutput(
1230
- <>
1231
- <span prop="A" />
1232
- <span prop="B" />
1233
- <span prop="C" />
1234
- </>,
1235
- );
1236
- });
1237
-
1238
- it('suspends inside constructor', async () => {
1239
- class AsyncTextInConstructor extends React.Component {
1240
- constructor(props) {
1241
- super(props);
1242
- const text = props.text;
1243
- Scheduler.unstable_yieldValue('constructor');
1244
- try {
1245
- TextResource.read([props.text, props.ms]);
1246
- this.state = {text};
1247
- } catch (promise) {
1248
- if (typeof promise.then === 'function') {
1249
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1250
- } else {
1251
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
1252
- }
1253
- throw promise;
1254
- }
1255
- }
1256
- componentDidMount() {
1257
- Scheduler.unstable_yieldValue('componentDidMount');
1258
- }
1259
- render() {
1260
- Scheduler.unstable_yieldValue(this.state.text);
1261
- return <span prop={this.state.text} />;
1262
- }
1263
- }
1264
-
1265
- ReactNoop.renderLegacySyncRoot(
1266
- <Suspense fallback={<Text text="Loading..." />}>
1267
- <AsyncTextInConstructor ms={100} text="Hi" />
1268
- </Suspense>,
1269
- );
1270
-
1271
- expect(Scheduler).toHaveYielded([
1272
- 'constructor',
1273
- 'Suspend! [Hi]',
1274
- 'Loading...',
1275
- ]);
1276
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1277
-
1278
- await advanceTimers(1000);
1279
-
1280
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1281
- expect(Scheduler).toFlushExpired([
1282
- 'constructor',
1283
- 'Hi',
1284
- 'componentDidMount',
1285
- ]);
1286
- expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1287
- });
1288
-
1289
- it('does not infinite loop if fallback contains lifecycle method', async () => {
1290
- class Fallback extends React.Component {
1291
- state = {
1292
- name: 'foo',
1293
- };
1294
- componentDidMount() {
1295
- this.setState({
1296
- name: 'bar',
1297
- });
1298
- }
1299
- render() {
1300
- return <Text text="Loading..." />;
1301
- }
1302
- }
1303
-
1304
- class Demo extends React.Component {
1305
- render() {
1306
- return (
1307
- <Suspense fallback={<Fallback />}>
1308
- <AsyncText text="Hi" ms={100} />
1309
- </Suspense>
1310
- );
1311
- }
1312
- }
1313
-
1314
- ReactNoop.renderLegacySyncRoot(<Demo />);
1315
-
1316
- expect(Scheduler).toHaveYielded([
1317
- 'Suspend! [Hi]',
1318
- 'Loading...',
1319
- // Re-render due to lifecycle update
1320
- 'Loading...',
1321
- ]);
1322
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1323
- await advanceTimers(100);
1324
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1325
- expect(Scheduler).toFlushExpired(['Hi']);
1326
- expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1327
- });
1328
-
1329
- if (global.__PERSISTENT__) {
1330
- it('hides/unhides suspended children before layout effects fire (persistent)', async () => {
1331
- const {useRef, useLayoutEffect} = React;
1332
-
1333
- function Parent() {
1334
- const child = useRef(null);
1335
-
1336
- useLayoutEffect(() => {
1337
- Scheduler.unstable_yieldValue(
1338
- ReactNoop.getPendingChildrenAsJSX(),
1339
- );
1340
- });
1341
-
1342
- return (
1343
- <span ref={child} hidden={false}>
1344
- <AsyncText ms={1000} text="Hi" />
1345
- </span>
1346
- );
1347
- }
1348
-
1349
- function App(props) {
1350
- return (
1351
- <Suspense fallback={<Text text="Loading..." />}>
1352
- <Parent />
1353
- </Suspense>
1354
- );
1355
- }
1356
-
1357
- ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
1358
-
1359
- expect(Scheduler).toHaveYielded([
1360
- 'Suspend! [Hi]',
1361
- 'Loading...',
1362
- // The child should have already been hidden
1363
- <>
1364
- <span hidden={true} />
1365
- <span prop="Loading..." />
1366
- </>,
1367
- ]);
1368
-
1369
- await advanceTimers(1000);
1370
-
1371
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1372
- expect(Scheduler).toFlushExpired(['Hi']);
1373
- });
1374
- } else {
1375
- it('hides/unhides suspended children before layout effects fire (mutation)', async () => {
1376
- const {useRef, useLayoutEffect} = React;
1377
-
1378
- function Parent() {
1379
- const child = useRef(null);
1380
-
1381
- useLayoutEffect(() => {
1382
- Scheduler.unstable_yieldValue(
1383
- 'Child is hidden: ' + child.current.hidden,
1384
- );
1385
- });
1386
-
1387
- return (
1388
- <span ref={child} hidden={false}>
1389
- <AsyncText ms={1000} text="Hi" />
1390
- </span>
1391
- );
1392
- }
1393
-
1394
- function App(props) {
1395
- return (
1396
- <Suspense fallback={<Text text="Loading..." />}>
1397
- <Parent />
1398
- </Suspense>
1399
- );
1400
- }
1401
-
1402
- ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
1403
-
1404
- expect(Scheduler).toHaveYielded([
1405
- 'Suspend! [Hi]',
1406
- 'Loading...',
1407
- // The child should have already been hidden
1408
- 'Child is hidden: true',
1409
- ]);
1410
-
1411
- await advanceTimers(1000);
1412
-
1413
- expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1414
- expect(Scheduler).toFlushExpired(['Hi']);
1415
- });
1416
- }
1417
-
1418
- it('handles errors in the return path of a component that suspends', async () => {
1419
- // Covers an edge case where an error is thrown inside the complete phase
1420
- // of a component that is in the return path of a component that suspends.
1421
- // The second error should also be handled (i.e. able to be captured by
1422
- // an error boundary.
1423
- class ErrorBoundary extends React.Component {
1424
- state = {error: null};
1425
- static getDerivedStateFromError(error, errorInfo) {
1426
- return {error};
1427
- }
1428
- render() {
1429
- if (this.state.error) {
1430
- return `Caught an error: ${this.state.error.message}`;
1431
- }
1432
- return this.props.children;
1433
- }
1434
- }
1039
+ }
1040
1436
- ReactNoop.renderLegacySyncRoot(
1437
- <ErrorBoundary>
1438
- <Suspense fallback="Loading...">
1439
- <errorInCompletePhase>
1440
- <AsyncText ms={1000} text="Async" />
1441
- </errorInCompletePhase>
1442
- </Suspense>
1443
- </ErrorBoundary>,
1444
- );
1041
+ // Times out immediately, ignoring the specified threshold.
1042
+ ReactNoop.renderLegacySyncRoot(<App />);
1043
+ expect(Scheduler).toHaveYielded(['Suspend! [Result]', 'Loading...']);
1044
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1045
1446
- expect(Scheduler).toHaveYielded(['Suspend! [Async]']);
1447
- expect(ReactNoop).toMatchRenderedOutput(
1448
- 'Caught an error: Error in host config.',
1449
- );
1450
- });
1451
-
1452
- it('does not drop mounted effects', async () => {
1453
- let never = {then() {}};
1454
-
1455
- let setShouldSuspend;
1456
- function App() {
1457
- const [shouldSuspend, _setShouldSuspend] = React.useState(0);
1458
- setShouldSuspend = _setShouldSuspend;
1459
- return (
1460
- <Suspense fallback="Loading...">
1461
- <Child shouldSuspend={shouldSuspend} />
1462
- </Suspense>
1463
- );
1464
- }
1046
+ ReactNoop.expire(100);
1047
+ await advanceTimers(100);
1048
1466
- function Child({shouldSuspend}) {
1467
- if (shouldSuspend) {
1468
- throw never;
1469
- }
1049
+ expect(Scheduler).toHaveYielded(['Promise resolved [Result]']);
1050
+ expect(Scheduler).toFlushExpired(['Result']);
1051
+ expect(ReactNoop.getChildren()).toEqual([span('Result')]);
1052
+ });
1053
1471
- React.useEffect(() => {
1472
- Scheduler.unstable_yieldValue('Mount');
1473
- return () => {
1474
- Scheduler.unstable_yieldValue('Unmount');
1475
- };
1476
- }, []);
1054
+ it('times out immediately when Suspense is in legacy mode', async () => {
1055
+ class UpdatingText extends React.Component {
1056
+ state = {step: 1};
1057
+ render() {
1058
+ return <AsyncText ms={100} text={`Step: ${this.state.step}`} />;
1059
+ }
1060
+ }
1061
1478
- return 'Child';
1479
- }
1062
+ function Spinner() {
1063
+ return (
1064
+ <Fragment>
1065
+ <Text text="Loading (1)" />
1066
+ <Text text="Loading (2)" />
1067
+ <Text text="Loading (3)" />
1068
+ </Fragment>
1069
+ );
1070
+ }
1071
1481
- const root = ReactNoop.createLegacyRoot(null);
1482
- await ReactNoop.act(async () => {
1483
- root.render(<App />);
1484
- });
1485
- expect(Scheduler).toHaveYielded(['Mount']);
1486
- expect(root).toMatchRenderedOutput('Child');
1072
+ const text = React.createRef(null);
1073
+ function App() {
1074
+ return (
1075
+ <Suspense fallback={<Spinner />}>
1076
+ <UpdatingText ref={text} />
1077
+ <Text text="Sibling" />
1078
+ </Suspense>
1079
+ );
1080
+ }
1081
1488
- // Suspend the child. This puts it into an inconsistent state.
1489
- await ReactNoop.act(async () => {
1490
- setShouldSuspend(true);
1491
- });
1492
- expect(root).toMatchRenderedOutput('Loading...');
1082
+ // Initial mount.
1083
+ ReactNoop.renderLegacySyncRoot(<App />);
1084
+ await advanceTimers(100);
1085
+ expect(Scheduler).toHaveYielded([
1086
+ 'Suspend! [Step: 1]',
1087
+ 'Sibling',
1088
+ 'Loading (1)',
1089
+ 'Loading (2)',
1090
+ 'Loading (3)',
1091
+ 'Promise resolved [Step: 1]',
1092
+ ]);
1093
+ expect(Scheduler).toFlushExpired(['Step: 1']);
1094
+ expect(ReactNoop).toMatchRenderedOutput(
1095
+ <>
1096
+ <span prop="Step: 1" />
1097
+ <span prop="Sibling" />
1098
+ </>,
1099
+ );
1100
1494
- // Unmount everying
1495
- await ReactNoop.act(async () => {
1496
- root.render(null);
1497
- });
1498
- expect(Scheduler).toHaveYielded(['Unmount']);
1499
- });
1500
- });
1101
+ // Update.
1102
+ text.current.setState({step: 2}, () =>
1103
+ Scheduler.unstable_yieldValue('Update did commit'),
1104
+ );
1105
1502
- it('does not call lifecycles of a suspended component', async () => {
1503
- class TextWithLifecycle extends React.Component {
1504
- componentDidMount() {
1505
- Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1506
- }
1507
- componentDidUpdate() {
1508
- Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1509
- }
1510
- componentWillUnmount() {
1511
- Scheduler.unstable_yieldValue(`Unmount [${this.props.text}]`);
1512
- }
1513
- render() {
1514
- return <Text {...this.props} />;
1515
- }
1516
- }
1106
+ expect(ReactNoop.flushNextYield()).toEqual([
1107
+ 'Suspend! [Step: 2]',
1108
+ 'Loading (1)',
1109
+ 'Loading (2)',
1110
+ 'Loading (3)',
1111
+ 'Update did commit',
1112
+ ]);
1113
+ expect(ReactNoop).toMatchRenderedOutput(
1114
+ <>
1115
+ <span hidden={true} prop="Step: 1" />
1116
+ <span hidden={true} prop="Sibling" />
1117
+ <span prop="Loading (1)" />
1118
+ <span prop="Loading (2)" />
1119
+ <span prop="Loading (3)" />
1120
+ </>,
1121
+ );
1122
1518
- class AsyncTextWithLifecycle extends React.Component {
1519
- componentDidMount() {
1520
- Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1521
- }
1522
- componentDidUpdate() {
1523
- Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1524
- }
1525
- componentWillUnmount() {
1526
- Scheduler.unstable_yieldValue(`Unmount [${this.props.text}]`);
1527
- }
1528
- render() {
1529
- const text = this.props.text;
1530
- const ms = this.props.ms;
1531
- try {
1532
- TextResource.read([text, ms]);
1533
- Scheduler.unstable_yieldValue(text);
1534
- return <span prop={text} />;
1535
- } catch (promise) {
1536
- if (typeof promise.then === 'function') {
1537
- Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1538
- } else {
1539
- Scheduler.unstable_yieldValue(`Error! [${text}]`);
1540
- }
1541
- throw promise;
1542
- }
1543
- }
1123
+ await advanceTimers(100);
1124
+ expect(Scheduler).toHaveYielded(['Promise resolved [Step: 2]']);
1125
+ expect(Scheduler).toFlushExpired(['Step: 2']);
1126
+ expect(ReactNoop).toMatchRenderedOutput(
1127
+ <>
1128
+ <span prop="Step: 2" />
1129
+ <span prop="Sibling" />
1130
+ </>,
1131
+ );
1132
+ });
1133
+
1134
+ it('does not re-render siblings in loose mode', async () => {
1135
+ class TextWithLifecycle extends React.Component {
1136
+ componentDidMount() {
1137
+ Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1138
}
1139
+ componentDidUpdate() {
1140
+ Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1141
+ }
1142
+ render() {
1143
+ return <Text {...this.props} />;
1144
+ }
1145
+ }
1146
1546
- function App() {
1547
- return (
1548
- <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1549
- <TextWithLifecycle text="A" />
1550
- <AsyncTextWithLifecycle ms={100} text="B" />
1551
- <TextWithLifecycle text="C" />
1552
- </Suspense>
1553
- );
1147
+ class AsyncTextWithLifecycle extends React.Component {
1148
+ componentDidMount() {
1149
+ Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1150
+ }
1151
+ componentDidUpdate() {
1152
+ Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1153
+ }
1154
+ render() {
1155
+ return <AsyncText {...this.props} />;
1156
}
1157
+ }
1158
1556
- ReactNoop.renderLegacySyncRoot(<App />, () =>
1557
- Scheduler.unstable_yieldValue('Commit root'),
1159
+ function App() {
1160
+ return (
1161
+ <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1162
+ <TextWithLifecycle text="A" />
1163
+ <AsyncTextWithLifecycle ms={100} text="B" />
1164
+ <TextWithLifecycle text="C" />
1165
+ </Suspense>
1166
);
1559
- expect(Scheduler).toHaveYielded([
1560
- 'A',
1561
- 'Suspend! [B]',
1562
- 'C',
1563
- 'Loading...',
1167
+ }
1168
1565
- 'Mount [A]',
1566
- // B's lifecycle should not fire because it suspended
1567
- // 'Mount [B]',
1568
- 'Mount [C]',
1569
- 'Mount [Loading...]',
1570
- 'Commit root',
1571
- ]);
1572
- expect(ReactNoop).toMatchRenderedOutput(
1573
- <>
1574
- <span hidden={true} prop="A" />
1575
- <span hidden={true} prop="C" />
1576
- <span prop="Loading..." />
1577
- </>,
1578
- );
1579
- });
1169
+ ReactNoop.renderLegacySyncRoot(<App />, () =>
1170
+ Scheduler.unstable_yieldValue('Commit root'),
1171
+ );
1172
+ expect(Scheduler).toHaveYielded([
1173
+ 'A',
1174
+ 'Suspend! [B]',
1175
+ 'C',
1176
+
1177
+ 'Loading...',
1178
+ 'Mount [A]',
1179
+ 'Mount [B]',
1180
+ 'Mount [C]',
1181
+ // This should be a mount, not an update.
1182
+ 'Mount [Loading...]',
1183
+ 'Commit root',
1184
+ ]);
1185
+ expect(ReactNoop).toMatchRenderedOutput(
1186
+ <>
1187
+ <span hidden={true} prop="A" />
1188
+ <span hidden={true} prop="C" />
1189
+
1190
+ <span prop="Loading..." />
1191
+ </>,
1192
+ );
1193
1581
- it('does not call lifecycles of a suspended component (hooks)', async () => {
1582
- function TextWithLifecycle(props) {
1583
- React.useLayoutEffect(() => {
1584
- Scheduler.unstable_yieldValue(`Layout Effect [${props.text}]`);
1585
- return () => {
1586
- Scheduler.unstable_yieldValue(
1587
- `Destroy Layout Effect [${props.text}]`,
1588
- );
1589
- };
1590
- }, [props.text]);
1591
- React.useEffect(() => {
1592
- Scheduler.unstable_yieldValue(`Effect [${props.text}]`);
1593
- return () => {
1594
- Scheduler.unstable_yieldValue(`Destroy Effect [${props.text}]`);
1595
- };
1596
- }, [props.text]);
1597
- return <Text {...props} />;
1598
- }
1194
+ ReactNoop.expire(1000);
1195
+ await advanceTimers(1000);
1196
+
1197
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1198
+ expect(Scheduler).toFlushExpired(['B']);
1199
+ expect(ReactNoop).toMatchRenderedOutput(
1200
+ <>
1201
+ <span prop="A" />
1202
+ <span prop="B" />
1203
+ <span prop="C" />
1204
+ </>,
1205
+ );
1206
+ });
1207
1600
- function AsyncTextWithLifecycle(props) {
1601
- React.useLayoutEffect(() => {
1602
- Scheduler.unstable_yieldValue(`Layout Effect [${props.text}]`);
1603
- return () => {
1604
- Scheduler.unstable_yieldValue(
1605
- `Destroy Layout Effect [${props.text}]`,
1606
- );
1607
- };
1608
- }, [props.text]);
1609
- React.useEffect(() => {
1610
- Scheduler.unstable_yieldValue(`Effect [${props.text}]`);
1611
- return () => {
1612
- Scheduler.unstable_yieldValue(`Destroy Effect [${props.text}]`);
1613
- };
1614
- }, [props.text]);
1208
+ it('suspends inside constructor', async () => {
1209
+ class AsyncTextInConstructor extends React.Component {
1210
+ constructor(props) {
1211
+ super(props);
1212
const text = props.text;
1616
- const ms = props.ms;
1213
+ Scheduler.unstable_yieldValue('constructor');
1214
try {
1618
- TextResource.read([text, ms]);
1619
- Scheduler.unstable_yieldValue(text);
1620
- return <span prop={text} />;
1215
+ TextResource.read([props.text, props.ms]);
1216
+ this.state = {text};
1217
} catch (promise) {
1218
if (typeof promise.then === 'function') {
1219
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
@@ -1627,1378 +1223,1725 @@ function loadModules({
1223
throw promise;
1224
}
1225
}
1630
-
1631
- function App({text}) {
1632
- return (
1633
- <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1634
- <TextWithLifecycle text="A" />
1635
- <AsyncTextWithLifecycle ms={100} text={text} />
1636
- <TextWithLifecycle text="C" />
1637
- </Suspense>
1638
- );
1226
+ componentDidMount() {
1227
+ Scheduler.unstable_yieldValue('componentDidMount');
1228
}
1229
+ render() {
1230
+ Scheduler.unstable_yieldValue(this.state.text);
1231
+ return <span prop={this.state.text} />;
1232
+ }
1233
+ }
1234
1641
- ReactNoop.renderLegacySyncRoot(<App text="B" />, () =>
1642
- Scheduler.unstable_yieldValue('Commit root'),
1643
- );
1644
- expect(Scheduler).toHaveYielded([
1645
- 'A',
1646
- 'Suspend! [B]',
1647
- 'C',
1648
- 'Loading...',
1649
-
1650
- 'Layout Effect [A]',
1651
- // B's effect should not fire because it suspended
1652
- // 'Layout Effect [B]',
1653
- 'Layout Effect [C]',
1654
- 'Layout Effect [Loading...]',
1655
- 'Commit root',
1656
- ]);
1235
+ ReactNoop.renderLegacySyncRoot(
1236
+ <Suspense fallback={<Text text="Loading..." />}>
1237
+ <AsyncTextInConstructor ms={100} text="Hi" />
1238
+ </Suspense>,
1239
+ );
1240
1658
- // Flush passive effects.
1659
- expect(Scheduler).toFlushAndYield([
1660
- 'Effect [A]',
1661
- // B's effect should not fire because it suspended
1662
- // 'Effect [B]',
1663
- 'Effect [C]',
1664
- 'Effect [Loading...]',
1665
- ]);
1241
+ expect(Scheduler).toHaveYielded([
1242
+ 'constructor',
1243
+ 'Suspend! [Hi]',
1244
+ 'Loading...',
1245
+ ]);
1246
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1247
+
1248
+ await advanceTimers(1000);
1249
+
1250
+ expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1251
+ expect(Scheduler).toFlushExpired([
1252
+ 'constructor',
1253
+ 'Hi',
1254
+ 'componentDidMount',
1255
+ ]);
1256
+ expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1257
+ });
1258
1667
- expect(ReactNoop).toMatchRenderedOutput(
1668
- <>
1669
- <span hidden={true} prop="A" />
1670
- <span hidden={true} prop="C" />
1671
- <span prop="Loading..." />
1672
- </>,
1673
- );
1259
+ it('does not infinite loop if fallback contains lifecycle method', async () => {
1260
+ class Fallback extends React.Component {
1261
+ state = {
1262
+ name: 'foo',
1263
+ };
1264
+ componentDidMount() {
1265
+ this.setState({
1266
+ name: 'bar',
1267
+ });
1268
+ }
1269
+ render() {
1270
+ return <Text text="Loading..." />;
1271
+ }
1272
+ }
1273
1675
- Scheduler.unstable_advanceTime(500);
1676
- await advanceTimers(500);
1677
-
1678
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1679
-
1680
- if (
1681
- ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount &&
1682
- ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates
1683
- ) {
1684
- expect(Scheduler).toFlushAndYield([
1685
- 'B',
1686
- 'Destroy Layout Effect [Loading...]',
1687
- 'Layout Effect [B]',
1688
- 'Destroy Effect [Loading...]',
1689
- 'Effect [B]',
1690
- ]);
1691
- } else {
1692
- expect(Scheduler).toFlushAndYield([
1693
- 'B',
1694
- 'Destroy Layout Effect [Loading...]',
1695
- 'Destroy Effect [Loading...]',
1696
- 'Layout Effect [B]',
1697
- 'Effect [B]',
1698
- ]);
1274
+ class Demo extends React.Component {
1275
+ render() {
1276
+ return (
1277
+ <Suspense fallback={<Fallback />}>
1278
+ <AsyncText text="Hi" ms={100} />
1279
+ </Suspense>
1280
+ );
1281
}
1282
+ }
1283
1701
- // Update
1702
- ReactNoop.renderLegacySyncRoot(<App text="B2" />, () =>
1703
- Scheduler.unstable_yieldValue('Commit root'),
1704
- );
1284
+ ReactNoop.renderLegacySyncRoot(<Demo />);
1285
+
1286
+ expect(Scheduler).toHaveYielded([
1287
+ 'Suspend! [Hi]',
1288
+ 'Loading...',
1289
+ // Re-render due to lifecycle update
1290
+ 'Loading...',
1291
+ ]);
1292
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1293
+ await advanceTimers(100);
1294
+ expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1295
+ expect(Scheduler).toFlushExpired(['Hi']);
1296
+ expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1297
+ });
1298
1706
- expect(Scheduler).toHaveYielded([
1707
- 'A',
1708
- 'Suspend! [B2]',
1709
- 'C',
1710
- 'Loading...',
1299
+ if (global.__PERSISTENT__) {
1300
+ it('hides/unhides suspended children before layout effects fire (persistent)', async () => {
1301
+ const {useRef, useLayoutEffect} = React;
1302
1712
- // B2's effect should not fire because it suspended
1713
- // 'Layout Effect [B2]',
1714
- 'Layout Effect [Loading...]',
1715
- 'Commit root',
1716
- ]);
1303
+ function Parent() {
1304
+ const child = useRef(null);
1305
1718
- // Flush passive effects.
1719
- expect(Scheduler).toFlushAndYield([
1720
- // B2's effect should not fire because it suspended
1721
- // 'Effect [B2]',
1722
- 'Effect [Loading...]',
1723
- ]);
1306
+ useLayoutEffect(() => {
1307
+ Scheduler.unstable_yieldValue(ReactNoop.getPendingChildrenAsJSX());
1308
+ });
1309
1725
- Scheduler.unstable_advanceTime(500);
1726
- await advanceTimers(500);
1727
-
1728
- expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
1729
-
1730
- if (
1731
- ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount &&
1732
- ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates
1733
- ) {
1734
- expect(Scheduler).toFlushAndYield([
1735
- 'B2',
1736
- 'Destroy Layout Effect [Loading...]',
1737
- 'Destroy Layout Effect [B]',
1738
- 'Layout Effect [B2]',
1739
- 'Destroy Effect [Loading...]',
1740
- 'Destroy Effect [B]',
1741
- 'Effect [B2]',
1742
- ]);
1743
- } else {
1744
- expect(Scheduler).toFlushAndYield([
1745
- 'B2',
1746
- 'Destroy Layout Effect [Loading...]',
1747
- 'Destroy Effect [Loading...]',
1748
- 'Destroy Layout Effect [B]',
1749
- 'Layout Effect [B2]',
1750
- 'Destroy Effect [B]',
1751
- 'Effect [B2]',
1752
- ]);
1310
+ return (
1311
+ <span ref={child} hidden={false}>
1312
+ <AsyncText ms={1000} text="Hi" />
1313
+ </span>
1314
+ );
1315
}
1754
- });
1316
1756
- it('suspends for longer if something took a long (CPU bound) time to render', async () => {
1757
- function Foo({renderContent}) {
1758
- Scheduler.unstable_yieldValue('Foo');
1317
+ function App(props) {
1318
return (
1319
<Suspense fallback={<Text text="Loading..." />}>
1761
- {renderContent ? <AsyncText text="A" ms={5000} /> : null}
1320
+ <Parent />
1321
</Suspense>
1322
);
1323
}
1324
1766
- ReactNoop.render(<Foo />);
1767
- expect(Scheduler).toFlushAndYield(['Foo']);
1768
-
1769
- ReactNoop.render(<Foo renderContent={true} />);
1770
- Scheduler.unstable_advanceTime(100);
1771
- await advanceTimers(100);
1772
- // Start rendering
1773
- expect(Scheduler).toFlushAndYieldThrough(['Foo']);
1774
- // For some reason it took a long time to render Foo.
1775
- Scheduler.unstable_advanceTime(1250);
1776
- await advanceTimers(1250);
1777
- expect(Scheduler).toFlushAndYield([
1778
- // A suspends
1779
- 'Suspend! [A]',
1325
+ ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
1326
+
1327
+ expect(Scheduler).toHaveYielded([
1328
+ 'Suspend! [Hi]',
1329
'Loading...',
1330
+ // The child should have already been hidden
1331
+ <>
1332
+ <span hidden={true} />
1333
+ <span prop="Loading..." />
1334
+ </>,
1335
]);
1782
- // We're now suspended and we haven't shown anything yet.
1783
- expect(ReactNoop.getChildren()).toEqual([]);
1784
-
1785
- // Flush some of the time
1786
- Scheduler.unstable_advanceTime(450);
1787
- await advanceTimers(450);
1788
- // Because we've already been waiting for so long we can
1789
- // wait a bit longer. Still nothing...
1790
- expect(Scheduler).toFlushWithoutYielding();
1791
- expect(ReactNoop.getChildren()).toEqual([]);
1792
-
1793
- // Eventually we'll show the fallback.
1794
- Scheduler.unstable_advanceTime(500);
1795
- await advanceTimers(500);
1796
- // No need to rerender.
1797
- expect(Scheduler).toFlushWithoutYielding();
1798
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1336
1800
- // Flush the promise completely
1801
- Scheduler.unstable_advanceTime(4500);
1802
- await advanceTimers(4500);
1803
- // Renders successfully
1804
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1805
- expect(Scheduler).toFlushAndYield(['A']);
1806
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
1337
+ await advanceTimers(1000);
1338
+
1339
+ expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1340
+ expect(Scheduler).toFlushExpired(['Hi']);
1341
});
1342
+ } else {
1343
+ it('hides/unhides suspended children before layout effects fire (mutation)', async () => {
1344
+ const {useRef, useLayoutEffect} = React;
1345
+
1346
+ function Parent() {
1347
+ const child = useRef(null);
1348
+
1349
+ useLayoutEffect(() => {
1350
+ Scheduler.unstable_yieldValue(
1351
+ 'Child is hidden: ' + child.current.hidden,
1352
+ );
1353
+ });
1354
1809
- it('does not suspends if a fallback has been shown for a long time', async () => {
1810
- function Foo() {
1811
- Scheduler.unstable_yieldValue('Foo');
1355
return (
1813
- <Suspense fallback={<Text text="Loading..." />}>
1814
- <AsyncText text="A" ms={5000} />
1815
- <Suspense fallback={<Text text="Loading more..." />}>
1816
- <AsyncText text="B" ms={10000} />
1817
- </Suspense>
1818
- </Suspense>
1356
+ <span ref={child} hidden={false}>
1357
+ <AsyncText ms={1000} text="Hi" />
1358
+ </span>
1359
);
1360
}
1361
1822
- ReactNoop.render(<Foo />);
1823
- // Start rendering
1824
- expect(Scheduler).toFlushAndYield([
1825
- 'Foo',
1826
- // A suspends
1827
- 'Suspend! [A]',
1828
- // B suspends
1829
- 'Suspend! [B]',
1830
- 'Loading more...',
1831
- 'Loading...',
1832
- ]);
1833
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1834
-
1835
- // Wait a long time.
1836
- Scheduler.unstable_advanceTime(5000);
1837
- await advanceTimers(5000);
1838
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1839
-
1840
- // Retry with the new content.
1841
- expect(Scheduler).toFlushAndYield([
1842
- 'A',
1843
- // B still suspends
1844
- 'Suspend! [B]',
1845
- 'Loading more...',
1846
- ]);
1847
- // Because we've already been waiting for so long we've exceeded
1848
- // our threshold and we show the next level immediately.
1849
- expect(ReactNoop.getChildren()).toEqual([
1850
- span('A'),
1851
- span('Loading more...'),
1852
- ]);
1853
-
1854
- // Flush the last promise completely
1855
- Scheduler.unstable_advanceTime(5000);
1856
- await advanceTimers(5000);
1857
- // Renders successfully
1858
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1859
- expect(Scheduler).toFlushAndYield(['B']);
1860
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1861
- });
1862
-
1863
- it('does suspend if a fallback has been shown for a short time', async () => {
1864
- function Foo() {
1865
- Scheduler.unstable_yieldValue('Foo');
1362
+ function App(props) {
1363
return (
1364
<Suspense fallback={<Text text="Loading..." />}>
1868
- <AsyncText text="A" ms={200} />
1869
- <Suspense fallback={<Text text="Loading more..." />}>
1870
- <AsyncText text="B" ms={450} />
1871
- </Suspense>
1365
+ <Parent />
1366
</Suspense>
1367
);
1368
}
1369
1876
- ReactNoop.render(<Foo />);
1877
- // Start rendering
1878
- expect(Scheduler).toFlushAndYield([
1879
- 'Foo',
1880
- // A suspends
1881
- 'Suspend! [A]',
1882
- // B suspends
1883
- 'Suspend! [B]',
1884
- 'Loading more...',
1885
- 'Loading...',
1886
- ]);
1887
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1370
+ ReactNoop.renderLegacySyncRoot(<App middleText="B" />);
1371
1889
- // Wait a short time.
1890
- Scheduler.unstable_advanceTime(250);
1891
- await advanceTimers(250);
1892
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1893
-
1894
- // Retry with the new content.
1895
- expect(Scheduler).toFlushAndYield([
1896
- 'A',
1897
- // B still suspends
1898
- 'Suspend! [B]',
1899
- 'Loading more...',
1372
+ expect(Scheduler).toHaveYielded([
1373
+ 'Suspend! [Hi]',
1374
+ 'Loading...',
1375
+ // The child should have already been hidden
1376
+ 'Child is hidden: true',
1377
]);
1901
- // Because we've already been waiting for so long we can
1902
- // wait a bit longer. Still nothing...
1903
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1378
1905
- Scheduler.unstable_advanceTime(200);
1906
- await advanceTimers(200);
1379
+ await advanceTimers(1000);
1380
1908
- // Before we commit another Promise resolves.
1909
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1910
- // We're still showing the first loading state.
1911
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1912
- // Restart and render the complete content.
1913
- expect(Scheduler).toFlushAndYield(['A', 'B']);
1914
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1381
+ expect(Scheduler).toHaveYielded(['Promise resolved [Hi]']);
1382
+ expect(Scheduler).toFlushExpired(['Hi']);
1383
});
1916
-
1917
- it('does not suspend for very long after a higher priority update', async () => {
1918
- function Foo({renderContent}) {
1919
- Scheduler.unstable_yieldValue('Foo');
1920
- return (
1921
- <Suspense fallback={<Text text="Loading..." />}>
1922
- {renderContent ? <AsyncText text="A" ms={5000} /> : null}
1923
- </Suspense>
1924
- );
1384
+ }
1385
+
1386
+ it('handles errors in the return path of a component that suspends', async () => {
1387
+ // Covers an edge case where an error is thrown inside the complete phase
1388
+ // of a component that is in the return path of a component that suspends.
1389
+ // The second error should also be handled (i.e. able to be captured by
1390
+ // an error boundary.
1391
+ class ErrorBoundary extends React.Component {
1392
+ state = {error: null};
1393
+ static getDerivedStateFromError(error, errorInfo) {
1394
+ return {error};
1395
}
1396
+ render() {
1397
+ if (this.state.error) {
1398
+ return `Caught an error: ${this.state.error.message}`;
1399
+ }
1400
+ return this.props.children;
1401
+ }
1402
+ }
1403
+
1404
+ ReactNoop.renderLegacySyncRoot(
1405
+ <ErrorBoundary>
1406
+ <Suspense fallback="Loading...">
1407
+ <errorInCompletePhase>
1408
+ <AsyncText ms={1000} text="Async" />
1409
+ </errorInCompletePhase>
1410
+ </Suspense>
1411
+ </ErrorBoundary>,
1412
+ );
1413
1927
- ReactNoop.render(<Foo />);
1928
- expect(Scheduler).toFlushAndYield(['Foo']);
1414
+ expect(Scheduler).toHaveYielded(['Suspend! [Async]']);
1415
+ expect(ReactNoop).toMatchRenderedOutput(
1416
+ 'Caught an error: Error in host config.',
1417
+ );
1418
+ });
1419
1930
- ReactNoop.discreteUpdates(() =>
1931
- ReactNoop.render(<Foo renderContent={true} />),
1420
+ it('does not drop mounted effects', async () => {
1421
+ let never = {then() {}};
1422
+
1423
+ let setShouldSuspend;
1424
+ function App() {
1425
+ const [shouldSuspend, _setShouldSuspend] = React.useState(0);
1426
+ setShouldSuspend = _setShouldSuspend;
1427
+ return (
1428
+ <Suspense fallback="Loading...">
1429
+ <Child shouldSuspend={shouldSuspend} />
1430
+ </Suspense>
1431
);
1933
- expect(Scheduler).toFlushAndYieldThrough(['Foo']);
1432
+ }
1433
1935
- // Advance some time.
1936
- Scheduler.unstable_advanceTime(100);
1937
- await advanceTimers(100);
1434
+ function Child({shouldSuspend}) {
1435
+ if (shouldSuspend) {
1436
+ throw never;
1437
+ }
1438
1939
- expect(Scheduler).toFlushAndYield([
1940
- // A suspends
1941
- 'Suspend! [A]',
1942
- 'Loading...',
1943
- ]);
1439
+ React.useEffect(() => {
1440
+ Scheduler.unstable_yieldValue('Mount');
1441
+ return () => {
1442
+ Scheduler.unstable_yieldValue('Unmount');
1443
+ };
1444
+ }, []);
1445
+
1446
+ return 'Child';
1447
+ }
1448
1945
- // We're now suspended and we haven't shown anything yet.
1946
- expect(ReactNoop.getChildren()).toEqual([]);
1449
+ const root = ReactNoop.createLegacyRoot(null);
1450
+ await ReactNoop.act(async () => {
1451
+ root.render(<App />);
1452
+ });
1453
+ expect(Scheduler).toHaveYielded(['Mount']);
1454
+ expect(root).toMatchRenderedOutput('Child');
1455
1948
- // Flush some of the time
1949
- Scheduler.unstable_advanceTime(500);
1950
- jest.advanceTimersByTime(500);
1456
+ // Suspend the child. This puts it into an inconsistent state.
1457
+ await ReactNoop.act(async () => {
1458
+ setShouldSuspend(true);
1459
+ });
1460
+ expect(root).toMatchRenderedOutput('Loading...');
1461
1952
- // We should have already shown the fallback.
1953
- // When we wrote this test, we inferred the start time of high priority
1954
- // updates as way earlier in the past. This test ensures that we don't
1955
- // use this assumption to add a very long JND.
1956
- expect(Scheduler).toFlushWithoutYielding();
1957
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1462
+ // Unmount everying
1463
+ await ReactNoop.act(async () => {
1464
+ root.render(null);
1465
});
1466
+ expect(Scheduler).toHaveYielded(['Unmount']);
1467
+ });
1468
+ });
1469
1960
- // TODO: flip to "warns" when this is implemented again.
1961
- it('does not warn when a low priority update suspends inside a high priority update for functional components', async () => {
1962
- let _setShow;
1963
- function App() {
1964
- let [show, setShow] = React.useState(false);
1965
- _setShow = setShow;
1966
- return (
1967
- <Suspense fallback="Loading...">
1968
- {show && <AsyncText text="A" />}
1969
- </Suspense>
1470
+ it('does not call lifecycles of a suspended component', async () => {
1471
+ class TextWithLifecycle extends React.Component {
1472
+ componentDidMount() {
1473
+ Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1474
+ }
1475
+ componentDidUpdate() {
1476
+ Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1477
+ }
1478
+ componentWillUnmount() {
1479
+ Scheduler.unstable_yieldValue(`Unmount [${this.props.text}]`);
1480
+ }
1481
+ render() {
1482
+ return <Text {...this.props} />;
1483
+ }
1484
+ }
1485
+
1486
+ class AsyncTextWithLifecycle extends React.Component {
1487
+ componentDidMount() {
1488
+ Scheduler.unstable_yieldValue(`Mount [${this.props.text}]`);
1489
+ }
1490
+ componentDidUpdate() {
1491
+ Scheduler.unstable_yieldValue(`Update [${this.props.text}]`);
1492
+ }
1493
+ componentWillUnmount() {
1494
+ Scheduler.unstable_yieldValue(`Unmount [${this.props.text}]`);
1495
+ }
1496
+ render() {
1497
+ const text = this.props.text;
1498
+ const ms = this.props.ms;
1499
+ try {
1500
+ TextResource.read([text, ms]);
1501
+ Scheduler.unstable_yieldValue(text);
1502
+ return <span prop={text} />;
1503
+ } catch (promise) {
1504
+ if (typeof promise.then === 'function') {
1505
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1506
+ } else {
1507
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
1508
+ }
1509
+ throw promise;
1510
+ }
1511
+ }
1512
+ }
1513
+
1514
+ function App() {
1515
+ return (
1516
+ <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1517
+ <TextWithLifecycle text="A" />
1518
+ <AsyncTextWithLifecycle ms={100} text="B" />
1519
+ <TextWithLifecycle text="C" />
1520
+ </Suspense>
1521
+ );
1522
+ }
1523
+
1524
+ ReactNoop.renderLegacySyncRoot(<App />, () =>
1525
+ Scheduler.unstable_yieldValue('Commit root'),
1526
+ );
1527
+ expect(Scheduler).toHaveYielded([
1528
+ 'A',
1529
+ 'Suspend! [B]',
1530
+ 'C',
1531
+ 'Loading...',
1532
+
1533
+ 'Mount [A]',
1534
+ // B's lifecycle should not fire because it suspended
1535
+ // 'Mount [B]',
1536
+ 'Mount [C]',
1537
+ 'Mount [Loading...]',
1538
+ 'Commit root',
1539
+ ]);
1540
+ expect(ReactNoop).toMatchRenderedOutput(
1541
+ <>
1542
+ <span hidden={true} prop="A" />
1543
+ <span hidden={true} prop="C" />
1544
+ <span prop="Loading..." />
1545
+ </>,
1546
+ );
1547
+ });
1548
+
1549
+ it('does not call lifecycles of a suspended component (hooks)', async () => {
1550
+ function TextWithLifecycle(props) {
1551
+ React.useLayoutEffect(() => {
1552
+ Scheduler.unstable_yieldValue(`Layout Effect [${props.text}]`);
1553
+ return () => {
1554
+ Scheduler.unstable_yieldValue(
1555
+ `Destroy Layout Effect [${props.text}]`,
1556
+ );
1557
+ };
1558
+ }, [props.text]);
1559
+ React.useEffect(() => {
1560
+ Scheduler.unstable_yieldValue(`Effect [${props.text}]`);
1561
+ return () => {
1562
+ Scheduler.unstable_yieldValue(`Destroy Effect [${props.text}]`);
1563
+ };
1564
+ }, [props.text]);
1565
+ return <Text {...props} />;
1566
+ }
1567
+
1568
+ function AsyncTextWithLifecycle(props) {
1569
+ React.useLayoutEffect(() => {
1570
+ Scheduler.unstable_yieldValue(`Layout Effect [${props.text}]`);
1571
+ return () => {
1572
+ Scheduler.unstable_yieldValue(
1573
+ `Destroy Layout Effect [${props.text}]`,
1574
);
1575
+ };
1576
+ }, [props.text]);
1577
+ React.useEffect(() => {
1578
+ Scheduler.unstable_yieldValue(`Effect [${props.text}]`);
1579
+ return () => {
1580
+ Scheduler.unstable_yieldValue(`Destroy Effect [${props.text}]`);
1581
+ };
1582
+ }, [props.text]);
1583
+ const text = props.text;
1584
+ const ms = props.ms;
1585
+ try {
1586
+ TextResource.read([text, ms]);
1587
+ Scheduler.unstable_yieldValue(text);
1588
+ return <span prop={text} />;
1589
+ } catch (promise) {
1590
+ if (typeof promise.then === 'function') {
1591
+ Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1592
+ } else {
1593
+ Scheduler.unstable_yieldValue(`Error! [${text}]`);
1594
}
1595
+ throw promise;
1596
+ }
1597
+ }
1598
+
1599
+ function App({text}) {
1600
+ return (
1601
+ <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1602
+ <TextWithLifecycle text="A" />
1603
+ <AsyncTextWithLifecycle ms={100} text={text} />
1604
+ <TextWithLifecycle text="C" />
1605
+ </Suspense>
1606
+ );
1607
+ }
1608
+
1609
+ ReactNoop.renderLegacySyncRoot(<App text="B" />, () =>
1610
+ Scheduler.unstable_yieldValue('Commit root'),
1611
+ );
1612
+ expect(Scheduler).toHaveYielded([
1613
+ 'A',
1614
+ 'Suspend! [B]',
1615
+ 'C',
1616
+ 'Loading...',
1617
+
1618
+ 'Layout Effect [A]',
1619
+ // B's effect should not fire because it suspended
1620
+ // 'Layout Effect [B]',
1621
+ 'Layout Effect [C]',
1622
+ 'Layout Effect [Loading...]',
1623
+ 'Commit root',
1624
+ ]);
1625
+
1626
+ // Flush passive effects.
1627
+ expect(Scheduler).toFlushAndYield([
1628
+ 'Effect [A]',
1629
+ // B's effect should not fire because it suspended
1630
+ // 'Effect [B]',
1631
+ 'Effect [C]',
1632
+ 'Effect [Loading...]',
1633
+ ]);
1634
+
1635
+ expect(ReactNoop).toMatchRenderedOutput(
1636
+ <>
1637
+ <span hidden={true} prop="A" />
1638
+ <span hidden={true} prop="C" />
1639
+ <span prop="Loading..." />
1640
+ </>,
1641
+ );
1642
+
1643
+ Scheduler.unstable_advanceTime(500);
1644
+ await advanceTimers(500);
1645
+
1646
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1647
+
1648
+ if (
1649
+ ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount &&
1650
+ ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates
1651
+ ) {
1652
+ expect(Scheduler).toFlushAndYield([
1653
+ 'B',
1654
+ 'Destroy Layout Effect [Loading...]',
1655
+ 'Layout Effect [B]',
1656
+ 'Destroy Effect [Loading...]',
1657
+ 'Effect [B]',
1658
+ ]);
1659
+ } else {
1660
+ expect(Scheduler).toFlushAndYield([
1661
+ 'B',
1662
+ 'Destroy Layout Effect [Loading...]',
1663
+ 'Destroy Effect [Loading...]',
1664
+ 'Layout Effect [B]',
1665
+ 'Effect [B]',
1666
+ ]);
1667
+ }
1668
+
1669
+ // Update
1670
+ ReactNoop.renderLegacySyncRoot(<App text="B2" />, () =>
1671
+ Scheduler.unstable_yieldValue('Commit root'),
1672
+ );
1673
+
1674
+ expect(Scheduler).toHaveYielded([
1675
+ 'A',
1676
+ 'Suspend! [B2]',
1677
+ 'C',
1678
+ 'Loading...',
1679
+
1680
+ // B2's effect should not fire because it suspended
1681
+ // 'Layout Effect [B2]',
1682
+ 'Layout Effect [Loading...]',
1683
+ 'Commit root',
1684
+ ]);
1685
+
1686
+ // Flush passive effects.
1687
+ expect(Scheduler).toFlushAndYield([
1688
+ // B2's effect should not fire because it suspended
1689
+ // 'Effect [B2]',
1690
+ 'Effect [Loading...]',
1691
+ ]);
1692
+
1693
+ Scheduler.unstable_advanceTime(500);
1694
+ await advanceTimers(500);
1695
+
1696
+ expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
1697
+
1698
+ if (
1699
+ ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount &&
1700
+ ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates
1701
+ ) {
1702
+ expect(Scheduler).toFlushAndYield([
1703
+ 'B2',
1704
+ 'Destroy Layout Effect [Loading...]',
1705
+ 'Destroy Layout Effect [B]',
1706
+ 'Layout Effect [B2]',
1707
+ 'Destroy Effect [Loading...]',
1708
+ 'Destroy Effect [B]',
1709
+ 'Effect [B2]',
1710
+ ]);
1711
+ } else {
1712
+ expect(Scheduler).toFlushAndYield([
1713
+ 'B2',
1714
+ 'Destroy Layout Effect [Loading...]',
1715
+ 'Destroy Effect [Loading...]',
1716
+ 'Destroy Layout Effect [B]',
1717
+ 'Layout Effect [B2]',
1718
+ 'Destroy Effect [B]',
1719
+ 'Effect [B2]',
1720
+ ]);
1721
+ }
1722
+ });
1723
1973
- await ReactNoop.act(async () => {
1974
- ReactNoop.render(<App />);
1975
- });
1724
+ it('suspends for longer if something took a long (CPU bound) time to render', async () => {
1725
+ function Foo({renderContent}) {
1726
+ Scheduler.unstable_yieldValue('Foo');
1727
+ return (
1728
+ <Suspense fallback={<Text text="Loading..." />}>
1729
+ {renderContent ? <AsyncText text="A" ms={5000} /> : null}
1730
+ </Suspense>
1731
+ );
1732
+ }
1733
+
1734
+ ReactNoop.render(<Foo />);
1735
+ expect(Scheduler).toFlushAndYield(['Foo']);
1736
+
1737
+ ReactNoop.render(<Foo renderContent={true} />);
1738
+ Scheduler.unstable_advanceTime(100);
1739
+ await advanceTimers(100);
1740
+ // Start rendering
1741
+ expect(Scheduler).toFlushAndYieldThrough(['Foo']);
1742
+ // For some reason it took a long time to render Foo.
1743
+ Scheduler.unstable_advanceTime(1250);
1744
+ await advanceTimers(1250);
1745
+ expect(Scheduler).toFlushAndYield([
1746
+ // A suspends
1747
+ 'Suspend! [A]',
1748
+ 'Loading...',
1749
+ ]);
1750
+ // We're now suspended and we haven't shown anything yet.
1751
+ expect(ReactNoop.getChildren()).toEqual([]);
1752
+
1753
+ // Flush some of the time
1754
+ Scheduler.unstable_advanceTime(450);
1755
+ await advanceTimers(450);
1756
+ // Because we've already been waiting for so long we can
1757
+ // wait a bit longer. Still nothing...
1758
+ expect(Scheduler).toFlushWithoutYielding();
1759
+ expect(ReactNoop.getChildren()).toEqual([]);
1760
+
1761
+ // Eventually we'll show the fallback.
1762
+ Scheduler.unstable_advanceTime(500);
1763
+ await advanceTimers(500);
1764
+ // No need to rerender.
1765
+ expect(Scheduler).toFlushWithoutYielding();
1766
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1767
+
1768
+ // Flush the promise completely
1769
+ Scheduler.unstable_advanceTime(4500);
1770
+ await advanceTimers(4500);
1771
+ // Renders successfully
1772
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1773
+ expect(Scheduler).toFlushAndYield(['A']);
1774
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
1775
+ });
1776
1977
- // TODO: assert toErrorDev() when the warning is implemented again.
1978
- ReactNoop.act(() => {
1979
- Scheduler.unstable_runWithPriority(
1980
- Scheduler.unstable_UserBlockingPriority,
1981
- () => _setShow(true),
1982
- );
1983
- });
1984
- });
1777
+ it('does not suspends if a fallback has been shown for a long time', async () => {
1778
+ function Foo() {
1779
+ Scheduler.unstable_yieldValue('Foo');
1780
+ return (
1781
+ <Suspense fallback={<Text text="Loading..." />}>
1782
+ <AsyncText text="A" ms={5000} />
1783
+ <Suspense fallback={<Text text="Loading more..." />}>
1784
+ <AsyncText text="B" ms={10000} />
1785
+ </Suspense>
1786
+ </Suspense>
1787
+ );
1788
+ }
1789
+
1790
+ ReactNoop.render(<Foo />);
1791
+ // Start rendering
1792
+ expect(Scheduler).toFlushAndYield([
1793
+ 'Foo',
1794
+ // A suspends
1795
+ 'Suspend! [A]',
1796
+ // B suspends
1797
+ 'Suspend! [B]',
1798
+ 'Loading more...',
1799
+ 'Loading...',
1800
+ ]);
1801
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1802
+
1803
+ // Wait a long time.
1804
+ Scheduler.unstable_advanceTime(5000);
1805
+ await advanceTimers(5000);
1806
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1807
+
1808
+ // Retry with the new content.
1809
+ expect(Scheduler).toFlushAndYield([
1810
+ 'A',
1811
+ // B still suspends
1812
+ 'Suspend! [B]',
1813
+ 'Loading more...',
1814
+ ]);
1815
+ // Because we've already been waiting for so long we've exceeded
1816
+ // our threshold and we show the next level immediately.
1817
+ expect(ReactNoop.getChildren()).toEqual([
1818
+ span('A'),
1819
+ span('Loading more...'),
1820
+ ]);
1821
+
1822
+ // Flush the last promise completely
1823
+ Scheduler.unstable_advanceTime(5000);
1824
+ await advanceTimers(5000);
1825
+ // Renders successfully
1826
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1827
+ expect(Scheduler).toFlushAndYield(['B']);
1828
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1829
+ });
1830
1986
- // TODO: flip to "warns" when this is implemented again.
1987
- it('does not warn when a low priority update suspends inside a high priority update for class components', async () => {
1988
- let show;
1989
- class App extends React.Component {
1990
- state = {show: false};
1991
-
1992
- render() {
1993
- show = () => this.setState({show: true});
1994
- return (
1995
- <Suspense fallback="Loading...">
1996
- {this.state.show && <AsyncText text="A" />}
1997
- </Suspense>
1998
- );
1999
- }
2000
- }
1831
+ it('does suspend if a fallback has been shown for a short time', async () => {
1832
+ function Foo() {
1833
+ Scheduler.unstable_yieldValue('Foo');
1834
+ return (
1835
+ <Suspense fallback={<Text text="Loading..." />}>
1836
+ <AsyncText text="A" ms={200} />
1837
+ <Suspense fallback={<Text text="Loading more..." />}>
1838
+ <AsyncText text="B" ms={450} />
1839
+ </Suspense>
1840
+ </Suspense>
1841
+ );
1842
+ }
1843
+
1844
+ ReactNoop.render(<Foo />);
1845
+ // Start rendering
1846
+ expect(Scheduler).toFlushAndYield([
1847
+ 'Foo',
1848
+ // A suspends
1849
+ 'Suspend! [A]',
1850
+ // B suspends
1851
+ 'Suspend! [B]',
1852
+ 'Loading more...',
1853
+ 'Loading...',
1854
+ ]);
1855
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1856
+
1857
+ // Wait a short time.
1858
+ Scheduler.unstable_advanceTime(250);
1859
+ await advanceTimers(250);
1860
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
1861
+
1862
+ // Retry with the new content.
1863
+ expect(Scheduler).toFlushAndYield([
1864
+ 'A',
1865
+ // B still suspends
1866
+ 'Suspend! [B]',
1867
+ 'Loading more...',
1868
+ ]);
1869
+ // Because we've already been waiting for so long we can
1870
+ // wait a bit longer. Still nothing...
1871
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1872
+
1873
+ Scheduler.unstable_advanceTime(200);
1874
+ await advanceTimers(200);
1875
+
1876
+ // Before we commit another Promise resolves.
1877
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1878
+ // We're still showing the first loading state.
1879
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1880
+ // Restart and render the complete content.
1881
+ expect(Scheduler).toFlushAndYield(['A', 'B']);
1882
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
1883
+ });
1884
2002
- await ReactNoop.act(async () => {
2003
- ReactNoop.render(<App />);
2004
- });
1885
+ it('does not suspend for very long after a higher priority update', async () => {
1886
+ function Foo({renderContent}) {
1887
+ Scheduler.unstable_yieldValue('Foo');
1888
+ return (
1889
+ <Suspense fallback={<Text text="Loading..." />}>
1890
+ {renderContent ? <AsyncText text="A" ms={5000} /> : null}
1891
+ </Suspense>
1892
+ );
1893
+ }
1894
+
1895
+ ReactNoop.render(<Foo />);
1896
+ expect(Scheduler).toFlushAndYield(['Foo']);
1897
+
1898
+ ReactNoop.discreteUpdates(() =>
1899
+ ReactNoop.render(<Foo renderContent={true} />),
1900
+ );
1901
+ expect(Scheduler).toFlushAndYieldThrough(['Foo']);
1902
+
1903
+ // Advance some time.
1904
+ Scheduler.unstable_advanceTime(100);
1905
+ await advanceTimers(100);
1906
+
1907
+ expect(Scheduler).toFlushAndYield([
1908
+ // A suspends
1909
+ 'Suspend! [A]',
1910
+ 'Loading...',
1911
+ ]);
1912
+
1913
+ // We're now suspended and we haven't shown anything yet.
1914
+ expect(ReactNoop.getChildren()).toEqual([]);
1915
+
1916
+ // Flush some of the time
1917
+ Scheduler.unstable_advanceTime(500);
1918
+ jest.advanceTimersByTime(500);
1919
+
1920
+ // We should have already shown the fallback.
1921
+ // When we wrote this test, we inferred the start time of high priority
1922
+ // updates as way earlier in the past. This test ensures that we don't
1923
+ // use this assumption to add a very long JND.
1924
+ expect(Scheduler).toFlushWithoutYielding();
1925
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1926
+ });
1927
2006
- // TODO: assert toErrorDev() when the warning is implemented again.
2007
- ReactNoop.act(() => {
2008
- Scheduler.unstable_runWithPriority(
2009
- Scheduler.unstable_UserBlockingPriority,
2010
- () => show(),
2011
- );
2012
- });
2013
- });
1928
+ // TODO: flip to "warns" when this is implemented again.
1929
+ it('does not warn when a low priority update suspends inside a high priority update for functional components', async () => {
1930
+ let _setShow;
1931
+ function App() {
1932
+ let [show, setShow] = React.useState(false);
1933
+ _setShow = setShow;
1934
+ return (
1935
+ <Suspense fallback="Loading...">
1936
+ {show && <AsyncText text="A" />}
1937
+ </Suspense>
1938
+ );
1939
+ }
1940
2015
- it('does not warn about wrong Suspense priority if no new fallbacks are shown', async () => {
2016
- let showB;
2017
- class App extends React.Component {
2018
- state = {showB: false};
2019
-
2020
- render() {
2021
- showB = () => this.setState({showB: true});
2022
- return (
2023
- <Suspense fallback="Loading...">
2024
- {<AsyncText text="A" />}
2025
- {this.state.showB && <AsyncText text="B" />}
2026
- </Suspense>
2027
- );
2028
- }
2029
- }
1941
+ await ReactNoop.act(async () => {
1942
+ ReactNoop.render(<App />);
1943
+ });
1944
2031
- await ReactNoop.act(async () => {
2032
- ReactNoop.render(<App />);
2033
- });
1945
+ // TODO: assert toErrorDev() when the warning is implemented again.
1946
+ ReactNoop.act(() => {
1947
+ Scheduler.unstable_runWithPriority(
1948
+ Scheduler.unstable_UserBlockingPriority,
1949
+ () => _setShow(true),
1950
+ );
1951
+ });
1952
+ });
1953
2035
- expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2036
- expect(ReactNoop).toMatchRenderedOutput('Loading...');
1954
+ // TODO: flip to "warns" when this is implemented again.
1955
+ it('does not warn when a low priority update suspends inside a high priority update for class components', async () => {
1956
+ let show;
1957
+ class App extends React.Component {
1958
+ state = {show: false};
1959
+
1960
+ render() {
1961
+ show = () => this.setState({show: true});
1962
+ return (
1963
+ <Suspense fallback="Loading...">
1964
+ {this.state.show && <AsyncText text="A" />}
1965
+ </Suspense>
1966
+ );
1967
+ }
1968
+ }
1969
2038
- ReactNoop.act(() => {
2039
- Scheduler.unstable_runWithPriority(
2040
- Scheduler.unstable_UserBlockingPriority,
2041
- () => showB(),
2042
- );
2043
- });
1970
+ await ReactNoop.act(async () => {
1971
+ ReactNoop.render(<App />);
1972
+ });
1973
2045
- expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Suspend! [B]']);
2046
- });
1974
+ // TODO: assert toErrorDev() when the warning is implemented again.
1975
+ ReactNoop.act(() => {
1976
+ Scheduler.unstable_runWithPriority(
1977
+ Scheduler.unstable_UserBlockingPriority,
1978
+ () => show(),
1979
+ );
1980
+ });
1981
+ });
1982
2048
- // TODO: flip to "warns" when this is implemented again.
2049
- it(
2050
- 'does not warn when component that triggered user-blocking update is between Suspense boundary ' +
2051
- 'and component that suspended',
2052
- async () => {
2053
- let _setShow;
2054
- function A() {
2055
- const [show, setShow] = React.useState(false);
2056
- _setShow = setShow;
2057
- return show && <AsyncText text="A" />;
2058
- }
2059
- function App() {
2060
- return (
2061
- <Suspense fallback="Loading...">
2062
- <A />
2063
- </Suspense>
2064
- );
2065
- }
2066
- await ReactNoop.act(async () => {
2067
- ReactNoop.render(<App />);
2068
- });
1983
+ it('does not warn about wrong Suspense priority if no new fallbacks are shown', async () => {
1984
+ let showB;
1985
+ class App extends React.Component {
1986
+ state = {showB: false};
1987
+
1988
+ render() {
1989
+ showB = () => this.setState({showB: true});
1990
+ return (
1991
+ <Suspense fallback="Loading...">
1992
+ {<AsyncText text="A" />}
1993
+ {this.state.showB && <AsyncText text="B" />}
1994
+ </Suspense>
1995
+ );
1996
+ }
1997
+ }
1998
2070
- // TODO: assert toErrorDev() when the warning is implemented again.
2071
- ReactNoop.act(() => {
2072
- Scheduler.unstable_runWithPriority(
2073
- Scheduler.unstable_UserBlockingPriority,
2074
- () => _setShow(true),
2075
- );
2076
- });
2077
- },
1999
+ await ReactNoop.act(async () => {
2000
+ ReactNoop.render(<App />);
2001
+ });
2002
+
2003
+ expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2004
+ expect(ReactNoop).toMatchRenderedOutput('Loading...');
2005
+
2006
+ ReactNoop.act(() => {
2007
+ Scheduler.unstable_runWithPriority(
2008
+ Scheduler.unstable_UserBlockingPriority,
2009
+ () => showB(),
2010
);
2011
+ });
2012
+
2013
+ expect(Scheduler).toHaveYielded(['Suspend! [A]', 'Suspend! [B]']);
2014
+ });
2015
2080
- it('normal priority updates suspending do not warn for class components', async () => {
2081
- let show;
2082
- class App extends React.Component {
2083
- state = {show: false};
2016
+ // TODO: flip to "warns" when this is implemented again.
2017
+ it(
2018
+ 'does not warn when component that triggered user-blocking update is between Suspense boundary ' +
2019
+ 'and component that suspended',
2020
+ async () => {
2021
+ let _setShow;
2022
+ function A() {
2023
+ const [show, setShow] = React.useState(false);
2024
+ _setShow = setShow;
2025
+ return show && <AsyncText text="A" />;
2026
+ }
2027
+ function App() {
2028
+ return (
2029
+ <Suspense fallback="Loading...">
2030
+ <A />
2031
+ </Suspense>
2032
+ );
2033
+ }
2034
+ await ReactNoop.act(async () => {
2035
+ ReactNoop.render(<App />);
2036
+ });
2037
2085
- render() {
2086
- show = () => this.setState({show: true});
2087
- return (
2088
- <Suspense fallback="Loading...">
2089
- {this.state.show && <AsyncText text="A" />}
2090
- </Suspense>
2091
- );
2092
- }
2093
- }
2038
+ // TODO: assert toErrorDev() when the warning is implemented again.
2039
+ ReactNoop.act(() => {
2040
+ Scheduler.unstable_runWithPriority(
2041
+ Scheduler.unstable_UserBlockingPriority,
2042
+ () => _setShow(true),
2043
+ );
2044
+ });
2045
+ },
2046
+ );
2047
2095
- await ReactNoop.act(async () => {
2096
- ReactNoop.render(<App />);
2097
- });
2048
+ it('normal priority updates suspending do not warn for class components', async () => {
2049
+ let show;
2050
+ class App extends React.Component {
2051
+ state = {show: false};
2052
+
2053
+ render() {
2054
+ show = () => this.setState({show: true});
2055
+ return (
2056
+ <Suspense fallback="Loading...">
2057
+ {this.state.show && <AsyncText text="A" />}
2058
+ </Suspense>
2059
+ );
2060
+ }
2061
+ }
2062
2099
- // also make sure lowpriority is okay
2100
- await ReactNoop.act(async () => show(true));
2063
+ await ReactNoop.act(async () => {
2064
+ ReactNoop.render(<App />);
2065
+ });
2066
2102
- expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2103
- Scheduler.unstable_advanceTime(100);
2104
- await advanceTimers(100);
2067
+ // also make sure lowpriority is okay
2068
+ await ReactNoop.act(async () => show(true));
2069
2106
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2107
- });
2070
+ expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2071
+ Scheduler.unstable_advanceTime(100);
2072
+ await advanceTimers(100);
2073
2109
- it('normal priority updates suspending do not warn for functional components', async () => {
2110
- let _setShow;
2111
- function App() {
2112
- let [show, setShow] = React.useState(false);
2113
- _setShow = setShow;
2114
- return (
2115
- <Suspense fallback="Loading...">
2116
- {show && <AsyncText text="A" />}
2117
- </Suspense>
2118
- );
2119
- }
2074
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2075
+ });
2076
2121
- await ReactNoop.act(async () => {
2122
- ReactNoop.render(<App />);
2123
- });
2077
+ it('normal priority updates suspending do not warn for functional components', async () => {
2078
+ let _setShow;
2079
+ function App() {
2080
+ let [show, setShow] = React.useState(false);
2081
+ _setShow = setShow;
2082
+ return (
2083
+ <Suspense fallback="Loading...">
2084
+ {show && <AsyncText text="A" />}
2085
+ </Suspense>
2086
+ );
2087
+ }
2088
2125
- // also make sure lowpriority is okay
2126
- await ReactNoop.act(async () => _setShow(true));
2089
+ await ReactNoop.act(async () => {
2090
+ ReactNoop.render(<App />);
2091
+ });
2092
2128
- expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2129
- Scheduler.unstable_advanceTime(100);
2130
- await advanceTimers(100);
2093
+ // also make sure lowpriority is okay
2094
+ await ReactNoop.act(async () => _setShow(true));
2095
2132
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2133
- });
2096
+ expect(Scheduler).toHaveYielded(['Suspend! [A]']);
2097
+ Scheduler.unstable_advanceTime(100);
2098
+ await advanceTimers(100);
2099
2135
- it('shows the parent fallback if the inner fallback should be avoided', async () => {
2136
- function Foo({showC}) {
2137
- Scheduler.unstable_yieldValue('Foo');
2138
- return (
2139
- <Suspense fallback={<Text text="Initial load..." />}>
2140
- <Suspense
2141
- unstable_avoidThisFallback={true}
2142
- fallback={<Text text="Updating..." />}>
2143
- <AsyncText text="A" ms={5000} />
2144
- {showC ? <AsyncText text="C" ms={5000} /> : null}
2145
- </Suspense>
2146
- <Text text="B" />
2147
- </Suspense>
2148
- );
2149
- }
2100
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2101
+ });
2102
2151
- ReactNoop.render(<Foo />);
2152
- expect(Scheduler).toFlushAndYield([
2153
- 'Foo',
2154
- 'Suspend! [A]',
2155
- 'B',
2156
- 'Initial load...',
2157
- ]);
2158
- expect(ReactNoop.getChildren()).toEqual([span('Initial load...')]);
2159
-
2160
- // Eventually we resolve and show the data.
2161
- Scheduler.unstable_advanceTime(5000);
2162
- await advanceTimers(5000);
2163
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2164
- expect(Scheduler).toFlushAndYield(['A', 'B']);
2165
- expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
2166
-
2167
- // Update to show C
2168
- ReactNoop.render(<Foo showC={true} />);
2169
- expect(Scheduler).toFlushAndYield([
2170
- 'Foo',
2171
- 'A',
2172
- 'Suspend! [C]',
2173
- 'Updating...',
2174
- 'B',
2175
- ]);
2176
- // Flush to skip suspended time.
2177
- Scheduler.unstable_advanceTime(600);
2178
- await advanceTimers(600);
2179
- // Since the optional suspense boundary is already showing its content,
2180
- // we have to use the inner fallback instead.
2181
- expect(ReactNoop.getChildren()).toEqual([
2182
- hiddenSpan('A'),
2183
- span('Updating...'),
2184
- span('B'),
2185
- ]);
2103
+ it('shows the parent fallback if the inner fallback should be avoided', async () => {
2104
+ function Foo({showC}) {
2105
+ Scheduler.unstable_yieldValue('Foo');
2106
+ return (
2107
+ <Suspense fallback={<Text text="Initial load..." />}>
2108
+ <Suspense
2109
+ unstable_avoidThisFallback={true}
2110
+ fallback={<Text text="Updating..." />}>
2111
+ <AsyncText text="A" ms={5000} />
2112
+ {showC ? <AsyncText text="C" ms={5000} /> : null}
2113
+ </Suspense>
2114
+ <Text text="B" />
2115
+ </Suspense>
2116
+ );
2117
+ }
2118
+
2119
+ ReactNoop.render(<Foo />);
2120
+ expect(Scheduler).toFlushAndYield([
2121
+ 'Foo',
2122
+ 'Suspend! [A]',
2123
+ 'B',
2124
+ 'Initial load...',
2125
+ ]);
2126
+ expect(ReactNoop.getChildren()).toEqual([span('Initial load...')]);
2127
+
2128
+ // Eventually we resolve and show the data.
2129
+ Scheduler.unstable_advanceTime(5000);
2130
+ await advanceTimers(5000);
2131
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2132
+ expect(Scheduler).toFlushAndYield(['A', 'B']);
2133
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]);
2134
+
2135
+ // Update to show C
2136
+ ReactNoop.render(<Foo showC={true} />);
2137
+ expect(Scheduler).toFlushAndYield([
2138
+ 'Foo',
2139
+ 'A',
2140
+ 'Suspend! [C]',
2141
+ 'Updating...',
2142
+ 'B',
2143
+ ]);
2144
+ // Flush to skip suspended time.
2145
+ Scheduler.unstable_advanceTime(600);
2146
+ await advanceTimers(600);
2147
+ // Since the optional suspense boundary is already showing its content,
2148
+ // we have to use the inner fallback instead.
2149
+ expect(ReactNoop.getChildren()).toEqual([
2150
+ hiddenSpan('A'),
2151
+ span('Updating...'),
2152
+ span('B'),
2153
+ ]);
2154
+
2155
+ // Later we load the data.
2156
+ Scheduler.unstable_advanceTime(5000);
2157
+ await advanceTimers(5000);
2158
+ expect(Scheduler).toHaveYielded(['Promise resolved [C]']);
2159
+ expect(Scheduler).toFlushAndYield(['A', 'C']);
2160
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('C'), span('B')]);
2161
+ });
2162
2187
- // Later we load the data.
2188
- Scheduler.unstable_advanceTime(5000);
2189
- await advanceTimers(5000);
2190
- expect(Scheduler).toHaveYielded(['Promise resolved [C]']);
2191
- expect(Scheduler).toFlushAndYield(['A', 'C']);
2192
- expect(ReactNoop.getChildren()).toEqual([
2193
- span('A'),
2194
- span('C'),
2195
- span('B'),
2196
- ]);
2197
- });
2163
+ it('favors showing the inner fallback for nested top level avoided fallback', async () => {
2164
+ function Foo({showB}) {
2165
+ Scheduler.unstable_yieldValue('Foo');
2166
+ return (
2167
+ <Suspense
2168
+ unstable_avoidThisFallback={true}
2169
+ fallback={<Text text="Loading A..." />}>
2170
+ <Text text="A" />
2171
+ <Suspense
2172
+ unstable_avoidThisFallback={true}
2173
+ fallback={<Text text="Loading B..." />}>
2174
+ <AsyncText text="B" ms={5000} />
2175
+ </Suspense>
2176
+ </Suspense>
2177
+ );
2178
+ }
2179
+
2180
+ ReactNoop.render(<Foo />);
2181
+ expect(Scheduler).toFlushAndYield([
2182
+ 'Foo',
2183
+ 'A',
2184
+ 'Suspend! [B]',
2185
+ 'Loading B...',
2186
+ ]);
2187
+ // Flush to skip suspended time.
2188
+ Scheduler.unstable_advanceTime(600);
2189
+ await advanceTimers(600);
2190
+
2191
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
2192
+ });
2193
2199
- it('favors showing the inner fallback for nested top level avoided fallback', async () => {
2200
- function Foo({showB}) {
2201
- Scheduler.unstable_yieldValue('Foo');
2202
- return (
2203
- <Suspense
2204
- unstable_avoidThisFallback={true}
2205
- fallback={<Text text="Loading A..." />}>
2206
- <Text text="A" />
2194
+ it('keeps showing an avoided parent fallback if it is already showing', async () => {
2195
+ function Foo({showB}) {
2196
+ Scheduler.unstable_yieldValue('Foo');
2197
+ return (
2198
+ <Suspense fallback={<Text text="Initial load..." />}>
2199
+ <Suspense
2200
+ unstable_avoidThisFallback={true}
2201
+ fallback={<Text text="Loading A..." />}>
2202
+ <Text text="A" />
2203
+ {showB ? (
2204
<Suspense
2205
unstable_avoidThisFallback={true}
2206
fallback={<Text text="Loading B..." />}>
2207
<AsyncText text="B" ms={5000} />
2208
</Suspense>
2212
- </Suspense>
2213
- );
2214
- }
2215
-
2216
- ReactNoop.render(<Foo />);
2217
- expect(Scheduler).toFlushAndYield([
2218
- 'Foo',
2219
- 'A',
2220
- 'Suspend! [B]',
2221
- 'Loading B...',
2222
- ]);
2223
- // Flush to skip suspended time.
2224
- Scheduler.unstable_advanceTime(600);
2225
- await advanceTimers(600);
2226
-
2227
- expect(ReactNoop.getChildren()).toEqual([
2228
- span('A'),
2229
- span('Loading B...'),
2230
- ]);
2231
- });
2232
-
2233
- it('keeps showing an avoided parent fallback if it is already showing', async () => {
2234
- function Foo({showB}) {
2235
- Scheduler.unstable_yieldValue('Foo');
2236
- return (
2237
- <Suspense fallback={<Text text="Initial load..." />}>
2238
- <Suspense
2239
- unstable_avoidThisFallback={true}
2240
- fallback={<Text text="Loading A..." />}>
2241
- <Text text="A" />
2242
- {showB ? (
2243
- <Suspense
2244
- unstable_avoidThisFallback={true}
2245
- fallback={<Text text="Loading B..." />}>
2246
- <AsyncText text="B" ms={5000} />
2247
- </Suspense>
2248
- ) : null}
2249
- </Suspense>
2250
- </Suspense>
2251
- );
2252
- }
2253
-
2254
- ReactNoop.render(<Foo />);
2255
- expect(Scheduler).toFlushAndYield(['Foo', 'A']);
2256
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2257
-
2258
- ReactNoop.render(<Foo showB={true} />);
2259
-
2260
- expect(Scheduler).toFlushAndYield([
2261
- 'Foo',
2262
- 'A',
2263
- 'Suspend! [B]',
2264
- 'Loading B...',
2265
- ]);
2266
- // Still suspended.
2267
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2268
-
2269
- // Flush to skip suspended time.
2270
- Scheduler.unstable_advanceTime(600);
2271
- await advanceTimers(600);
2272
-
2273
- expect(ReactNoop.getChildren()).toEqual([
2274
- span('A'),
2275
- span('Loading B...'),
2276
- ]);
2277
- });
2278
-
2279
- it('commits a suspended idle pri render within a reasonable time', async () => {
2280
- function Foo({renderContent}) {
2281
- return (
2282
- <Fragment>
2283
- <Suspense fallback={<Text text="Loading A..." />}>
2284
- {renderContent ? <AsyncText text="A" ms={10000} /> : null}
2285
- </Suspense>
2286
- </Fragment>
2287
- );
2288
- }
2209
+ ) : null}
2210
+ </Suspense>
2211
+ </Suspense>
2212
+ );
2213
+ }
2214
2290
- ReactNoop.render(<Foo />);
2291
- expect(Scheduler).toFlushAndYield([]);
2215
+ ReactNoop.render(<Foo />);
2216
+ expect(Scheduler).toFlushAndYield(['Foo', 'A']);
2217
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2218
2293
- ReactNoop.render(<Foo renderContent={1} />);
2219
+ ReactNoop.render(<Foo showB={true} />);
2220
2295
- // Took a long time to render. This is to ensure we get a long suspense time.
2296
- // Could also use something like withSuspenseConfig to simulate this.
2297
- Scheduler.unstable_advanceTime(1500);
2298
- await advanceTimers(1500);
2221
+ expect(Scheduler).toFlushAndYield([
2222
+ 'Foo',
2223
+ 'A',
2224
+ 'Suspend! [B]',
2225
+ 'Loading B...',
2226
+ ]);
2227
+ // Still suspended.
2228
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2229
2300
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading A...']);
2301
- // We're still suspended.
2302
- expect(ReactNoop.getChildren()).toEqual([]);
2230
+ // Flush to skip suspended time.
2231
+ Scheduler.unstable_advanceTime(600);
2232
+ await advanceTimers(600);
2233
2304
- // Schedule an update at idle pri.
2305
- Scheduler.unstable_runWithPriority(
2306
- Scheduler.unstable_IdlePriority,
2307
- () => ReactNoop.render(<Foo renderContent={2} />),
2308
- );
2309
- // We won't even work on Idle priority.
2310
- expect(Scheduler).toFlushAndYield([]);
2234
+ expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]);
2235
+ });
2236
2312
- // We're still suspended.
2313
- expect(ReactNoop.getChildren()).toEqual([]);
2237
+ it('commits a suspended idle pri render within a reasonable time', async () => {
2238
+ function Foo({renderContent}) {
2239
+ return (
2240
+ <Fragment>
2241
+ <Suspense fallback={<Text text="Loading A..." />}>
2242
+ {renderContent ? <AsyncText text="A" ms={10000} /> : null}
2243
+ </Suspense>
2244
+ </Fragment>
2245
+ );
2246
+ }
2247
2315
- // Advance time a little bit.
2316
- Scheduler.unstable_advanceTime(150);
2317
- await advanceTimers(150);
2248
+ ReactNoop.render(<Foo />);
2249
+ expect(Scheduler).toFlushAndYield([]);
2250
2319
- // We should not have committed yet because we had a long suspense time.
2320
- expect(ReactNoop.getChildren()).toEqual([]);
2251
+ ReactNoop.render(<Foo renderContent={1} />);
2252
2322
- // Flush to skip suspended time.
2323
- Scheduler.unstable_advanceTime(600);
2324
- await advanceTimers(600);
2253
+ // Took a long time to render. This is to ensure we get a long suspense time.
2254
+ // Could also use something like withSuspenseConfig to simulate this.
2255
+ Scheduler.unstable_advanceTime(1500);
2256
+ await advanceTimers(1500);
2257
2326
- expect(ReactNoop.getChildren()).toEqual([span('Loading A...')]);
2327
- });
2258
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading A...']);
2259
+ // We're still suspended.
2260
+ expect(ReactNoop.getChildren()).toEqual([]);
2261
2329
- describe('delays transitions when there a suspense config is supplied', () => {
2330
- const SUSPENSE_CONFIG = {
2331
- timeoutMs: 2000,
2332
- };
2262
+ // Schedule an update at idle pri.
2263
+ Scheduler.unstable_runWithPriority(Scheduler.unstable_IdlePriority, () =>
2264
+ ReactNoop.render(<Foo renderContent={2} />),
2265
+ );
2266
+ // We won't even work on Idle priority.
2267
+ expect(Scheduler).toFlushAndYield([]);
2268
2334
- it('top level render', async () => {
2335
- function App({page}) {
2336
- return (
2337
- <Suspense fallback={<Text text="Loading..." />}>
2338
- <AsyncText text={page} ms={5000} />
2339
- </Suspense>
2340
- );
2341
- }
2269
+ // We're still suspended.
2270
+ expect(ReactNoop.getChildren()).toEqual([]);
2271
2343
- // Initial render.
2344
- React.unstable_withSuspenseConfig(
2345
- () => ReactNoop.render(<App page="A" />),
2346
- SUSPENSE_CONFIG,
2347
- );
2272
+ // Advance time a little bit.
2273
+ Scheduler.unstable_advanceTime(150);
2274
+ await advanceTimers(150);
2275
2349
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2350
- // Only a short time is needed to unsuspend the initial loading state.
2351
- Scheduler.unstable_advanceTime(400);
2352
- await advanceTimers(400);
2353
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2354
-
2355
- // Later we load the data.
2356
- Scheduler.unstable_advanceTime(5000);
2357
- await advanceTimers(5000);
2358
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2359
- expect(Scheduler).toFlushAndYield(['A']);
2360
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2361
-
2362
- // Start transition.
2363
- React.unstable_withSuspenseConfig(
2364
- () => ReactNoop.render(<App page="B" />),
2365
- SUSPENSE_CONFIG,
2366
- );
2276
+ // We should not have committed yet because we had a long suspense time.
2277
+ expect(ReactNoop.getChildren()).toEqual([]);
2278
2368
- expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2369
- Scheduler.unstable_advanceTime(1000);
2370
- await advanceTimers(1000);
2371
- // Even after a second, we have still not yet flushed the loading state.
2372
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2373
- Scheduler.unstable_advanceTime(1100);
2374
- await advanceTimers(1100);
2375
- // After the timeout, we do show the loading state.
2376
- expect(ReactNoop.getChildren()).toEqual([
2377
- hiddenSpan('A'),
2378
- span('Loading...'),
2379
- ]);
2380
- // Later we load the data.
2381
- Scheduler.unstable_advanceTime(3000);
2382
- await advanceTimers(3000);
2383
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2384
- expect(Scheduler).toFlushAndYield(['B']);
2385
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2386
- });
2387
-
2388
- it('hooks', async () => {
2389
- let transitionToPage;
2390
- function App() {
2391
- let [page, setPage] = React.useState('none');
2392
- transitionToPage = setPage;
2393
- if (page === 'none') {
2394
- return null;
2395
- }
2396
- return (
2397
- <Suspense fallback={<Text text="Loading..." />}>
2398
- <AsyncText text={page} ms={5000} />
2399
- </Suspense>
2400
- );
2401
- }
2279
+ // Flush to skip suspended time.
2280
+ Scheduler.unstable_advanceTime(600);
2281
+ await advanceTimers(600);
2282
2403
- ReactNoop.render(<App />);
2404
- expect(Scheduler).toFlushAndYield([]);
2283
+ expect(ReactNoop.getChildren()).toEqual([span('Loading A...')]);
2284
+ });
2285
2406
- // Initial render.
2407
- await ReactNoop.act(async () => {
2408
- React.unstable_withSuspenseConfig(
2409
- () => transitionToPage('A'),
2410
- SUSPENSE_CONFIG,
2411
- );
2286
+ describe('delays transitions when there a suspense config is supplied', () => {
2287
+ const SUSPENSE_CONFIG = {
2288
+ timeoutMs: 2000,
2289
+ };
2290
2413
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2414
- // Only a short time is needed to unsuspend the initial loading state.
2415
- Scheduler.unstable_advanceTime(400);
2416
- await advanceTimers(400);
2417
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2418
- });
2291
+ it('top level render', async () => {
2292
+ function App({page}) {
2293
+ return (
2294
+ <Suspense fallback={<Text text="Loading..." />}>
2295
+ <AsyncText text={page} ms={5000} />
2296
+ </Suspense>
2297
+ );
2298
+ }
2299
2420
- // Later we load the data.
2421
- Scheduler.unstable_advanceTime(5000);
2422
- await advanceTimers(5000);
2423
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2424
- expect(Scheduler).toFlushAndYield(['A']);
2425
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2426
-
2427
- // Start transition.
2428
- await ReactNoop.act(async () => {
2429
- React.unstable_withSuspenseConfig(
2430
- () => transitionToPage('B'),
2431
- SUSPENSE_CONFIG,
2432
- );
2300
+ // Initial render.
2301
+ React.unstable_withSuspenseConfig(
2302
+ () => ReactNoop.render(<App page="A" />),
2303
+ SUSPENSE_CONFIG,
2304
+ );
2305
2434
- expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2435
- Scheduler.unstable_advanceTime(1000);
2436
- await advanceTimers(1000);
2437
- // Even after a second, we have still not yet flushed the loading state.
2438
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2439
- Scheduler.unstable_advanceTime(1100);
2440
- await advanceTimers(1100);
2441
- // After the timeout, we do show the loading state.
2442
- expect(ReactNoop.getChildren()).toEqual([
2443
- hiddenSpan('A'),
2444
- span('Loading...'),
2445
- ]);
2446
- });
2447
- // Later we load the data.
2448
- Scheduler.unstable_advanceTime(3000);
2449
- await advanceTimers(3000);
2450
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2451
- expect(Scheduler).toFlushAndYield(['B']);
2452
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2453
- });
2454
-
2455
- it('classes', async () => {
2456
- let transitionToPage;
2457
- class App extends React.Component {
2458
- state = {page: 'none'};
2459
- render() {
2460
- transitionToPage = page => this.setState({page});
2461
- let page = this.state.page;
2462
- if (page === 'none') {
2463
- return null;
2464
- }
2465
- return (
2466
- <Suspense fallback={<Text text="Loading..." />}>
2467
- <AsyncText text={page} ms={5000} />
2468
- </Suspense>
2469
- );
2470
- }
2471
- }
2306
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2307
+ // Only a short time is needed to unsuspend the initial loading state.
2308
+ Scheduler.unstable_advanceTime(400);
2309
+ await advanceTimers(400);
2310
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2311
+
2312
+ // Later we load the data.
2313
+ Scheduler.unstable_advanceTime(5000);
2314
+ await advanceTimers(5000);
2315
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2316
+ expect(Scheduler).toFlushAndYield(['A']);
2317
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2318
+
2319
+ // Start transition.
2320
+ React.unstable_withSuspenseConfig(
2321
+ () => ReactNoop.render(<App page="B" />),
2322
+ SUSPENSE_CONFIG,
2323
+ );
2324
2473
- ReactNoop.render(<App />);
2474
- expect(Scheduler).toFlushAndYield([]);
2325
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2326
+ Scheduler.unstable_advanceTime(1000);
2327
+ await advanceTimers(1000);
2328
+ // Even after a second, we have still not yet flushed the loading state.
2329
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2330
+ Scheduler.unstable_advanceTime(1100);
2331
+ await advanceTimers(1100);
2332
+ // After the timeout, we do show the loading state.
2333
+ expect(ReactNoop.getChildren()).toEqual([
2334
+ hiddenSpan('A'),
2335
+ span('Loading...'),
2336
+ ]);
2337
+ // Later we load the data.
2338
+ Scheduler.unstable_advanceTime(3000);
2339
+ await advanceTimers(3000);
2340
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2341
+ expect(Scheduler).toFlushAndYield(['B']);
2342
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2343
+ });
2344
2476
- // Initial render.
2477
- await ReactNoop.act(async () => {
2478
- React.unstable_withSuspenseConfig(
2479
- () => transitionToPage('A'),
2480
- SUSPENSE_CONFIG,
2481
- );
2345
+ it('hooks', async () => {
2346
+ let transitionToPage;
2347
+ function App() {
2348
+ let [page, setPage] = React.useState('none');
2349
+ transitionToPage = setPage;
2350
+ if (page === 'none') {
2351
+ return null;
2352
+ }
2353
+ return (
2354
+ <Suspense fallback={<Text text="Loading..." />}>
2355
+ <AsyncText text={page} ms={5000} />
2356
+ </Suspense>
2357
+ );
2358
+ }
2359
2483
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2484
- // Only a short time is needed to unsuspend the initial loading state.
2485
- Scheduler.unstable_advanceTime(400);
2486
- await advanceTimers(400);
2487
- expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2488
- });
2360
+ ReactNoop.render(<App />);
2361
+ expect(Scheduler).toFlushAndYield([]);
2362
2490
- // Later we load the data.
2491
- Scheduler.unstable_advanceTime(5000);
2492
- await advanceTimers(5000);
2493
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2494
- expect(Scheduler).toFlushAndYield(['A']);
2495
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2496
-
2497
- // Start transition.
2498
- await ReactNoop.act(async () => {
2499
- React.unstable_withSuspenseConfig(
2500
- () => transitionToPage('B'),
2501
- SUSPENSE_CONFIG,
2502
- );
2363
+ // Initial render.
2364
+ await ReactNoop.act(async () => {
2365
+ React.unstable_withSuspenseConfig(
2366
+ () => transitionToPage('A'),
2367
+ SUSPENSE_CONFIG,
2368
+ );
2369
2504
- expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2505
- Scheduler.unstable_advanceTime(1000);
2506
- await advanceTimers(1000);
2507
- // Even after a second, we have still not yet flushed the loading state.
2508
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2509
- Scheduler.unstable_advanceTime(1100);
2510
- await advanceTimers(1100);
2511
- // After the timeout, we do show the loading state.
2512
- expect(ReactNoop.getChildren()).toEqual([
2513
- hiddenSpan('A'),
2514
- span('Loading...'),
2515
- ]);
2516
- });
2517
- // Later we load the data.
2518
- Scheduler.unstable_advanceTime(3000);
2519
- await advanceTimers(3000);
2520
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2521
- expect(Scheduler).toFlushAndYield(['B']);
2522
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2523
- });
2370
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2371
+ // Only a short time is needed to unsuspend the initial loading state.
2372
+ Scheduler.unstable_advanceTime(400);
2373
+ await advanceTimers(400);
2374
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2375
});
2376
2526
- it('disables suspense config when nothing is passed to withSuspenseConfig', async () => {
2527
- function App({page}) {
2528
- return (
2529
- <Suspense fallback={<Text text="Loading..." />}>
2530
- <AsyncText text={page} ms={2000} />
2531
- </Suspense>
2532
- );
2533
- }
2534
-
2535
- // Initial render.
2536
- ReactNoop.render(<App page="A" />);
2537
- expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2538
- Scheduler.unstable_advanceTime(2000);
2539
- await advanceTimers(2000);
2540
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2541
- expect(Scheduler).toFlushAndYield(['A']);
2542
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2377
+ // Later we load the data.
2378
+ Scheduler.unstable_advanceTime(5000);
2379
+ await advanceTimers(5000);
2380
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2381
+ expect(Scheduler).toFlushAndYield(['A']);
2382
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2383
2544
- // Start transition.
2384
+ // Start transition.
2385
+ await ReactNoop.act(async () => {
2386
React.unstable_withSuspenseConfig(
2546
- () => {
2547
- // When we schedule an inner transition without a suspense config
2548
- // so it should only suspend for a short time.
2549
- React.unstable_withSuspenseConfig(() =>
2550
- ReactNoop.render(<App page="B" />),
2551
- );
2552
- },
2553
- {timeoutMs: 2000},
2387
+ () => transitionToPage('B'),
2388
+ SUSPENSE_CONFIG,
2389
);
2390
2391
expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2557
- // Suspended
2392
+ Scheduler.unstable_advanceTime(1000);
2393
+ await advanceTimers(1000);
2394
+ // Even after a second, we have still not yet flushed the loading state.
2395
expect(ReactNoop.getChildren()).toEqual([span('A')]);
2559
- Scheduler.unstable_advanceTime(500);
2560
- await advanceTimers(500);
2561
- // Committed loading state.
2396
+ Scheduler.unstable_advanceTime(1100);
2397
+ await advanceTimers(1100);
2398
+ // After the timeout, we do show the loading state.
2399
expect(ReactNoop.getChildren()).toEqual([
2400
hiddenSpan('A'),
2401
span('Loading...'),
2402
]);
2566
-
2567
- Scheduler.unstable_advanceTime(2000);
2568
- await advanceTimers(2000);
2569
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2570
- expect(Scheduler).toFlushAndYield(['B']);
2571
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2572
-
2573
- React.unstable_withSuspenseConfig(
2574
- () => {
2575
- // First we schedule an inner unrelated update.
2576
- React.unstable_withSuspenseConfig(() =>
2577
- ReactNoop.render(<App page="B" unrelated={true} />),
2578
- );
2579
- // Then we schedule another transition to a slow page,
2580
- // but at this scope we should suspend for longer.
2581
- Scheduler.unstable_next(() => ReactNoop.render(<App page="C" />));
2582
- },
2583
- {timeoutMs: 2000},
2584
- );
2585
- expect(Scheduler).toFlushAndYield([
2586
- 'Suspend! [C]',
2587
- 'Loading...',
2588
- 'Suspend! [C]',
2589
- 'Loading...',
2590
- ]);
2591
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2592
- Scheduler.unstable_advanceTime(1200);
2593
- await advanceTimers(1200);
2594
- // Even after a second, we have still not yet flushed the loading state.
2595
- expect(ReactNoop.getChildren()).toEqual([span('B')]);
2596
- Scheduler.unstable_advanceTime(1200);
2597
- await advanceTimers(1200);
2598
- // After the two second timeout we show the loading state.
2599
- expect(ReactNoop.getChildren()).toEqual([
2600
- hiddenSpan('B'),
2601
- span('Loading...'),
2602
- ]);
2403
});
2404
+ // Later we load the data.
2405
+ Scheduler.unstable_advanceTime(3000);
2406
+ await advanceTimers(3000);
2407
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2408
+ expect(Scheduler).toFlushAndYield(['B']);
2409
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2410
+ });
2411
2605
- it('withSuspenseConfig timeout applies when we use an updated avoided boundary', async () => {
2606
- function App({page}) {
2412
+ it('classes', async () => {
2413
+ let transitionToPage;
2414
+ class App extends React.Component {
2415
+ state = {page: 'none'};
2416
+ render() {
2417
+ transitionToPage = page => this.setState({page});
2418
+ let page = this.state.page;
2419
+ if (page === 'none') {
2420
+ return null;
2421
+ }
2422
return (
2423
<Suspense fallback={<Text text="Loading..." />}>
2609
- <Text text="Hi!" />
2610
- <Suspense
2611
- fallback={<Text text={'Loading ' + page + '...'} />}
2612
- unstable_avoidThisFallback={true}>
2613
- <AsyncText text={page} ms={3000} />
2614
- </Suspense>
2424
+ <AsyncText text={page} ms={5000} />
2425
</Suspense>
2426
);
2427
}
2428
+ }
2429
2619
- // Initial render.
2620
- ReactNoop.render(<App page="A" />);
2621
- expect(Scheduler).toFlushAndYield([
2622
- 'Hi!',
2623
- 'Suspend! [A]',
2624
- 'Loading...',
2625
- ]);
2626
- Scheduler.unstable_advanceTime(3000);
2627
- await advanceTimers(3000);
2628
- expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2629
- expect(Scheduler).toFlushAndYield(['Hi!', 'A']);
2630
- expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2430
+ ReactNoop.render(<App />);
2431
+ expect(Scheduler).toFlushAndYield([]);
2432
2632
- // Start transition.
2433
+ // Initial render.
2434
+ await ReactNoop.act(async () => {
2435
React.unstable_withSuspenseConfig(
2634
- () => ReactNoop.render(<App page="B" />),
2635
- {timeoutMs: 2000},
2436
+ () => transitionToPage('A'),
2437
+ SUSPENSE_CONFIG,
2438
);
2439
2638
- expect(Scheduler).toFlushAndYield([
2639
- 'Hi!',
2640
- 'Suspend! [B]',
2641
- 'Loading B...',
2642
- ]);
2643
-
2644
- // Suspended
2645
- expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2646
- Scheduler.unstable_advanceTime(1800);
2647
- await advanceTimers(1800);
2648
- expect(Scheduler).toFlushAndYield([]);
2649
- // We should still be suspended here because this loading state should be avoided.
2650
- expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2651
- Scheduler.unstable_advanceTime(1500);
2652
- await advanceTimers(1500);
2653
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2654
- expect(ReactNoop.getChildren()).toEqual([
2655
- span('Hi!'),
2656
- hiddenSpan('A'),
2657
- span('Loading B...'),
2658
- ]);
2440
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2441
+ // Only a short time is needed to unsuspend the initial loading state.
2442
+ Scheduler.unstable_advanceTime(400);
2443
+ await advanceTimers(400);
2444
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2445
});
2446
2661
- it('withSuspenseConfig timeout applies when we use a newly created avoided boundary', async () => {
2662
- function App({page}) {
2663
- return (
2664
- <Suspense fallback={<Text text="Loading..." />}>
2665
- <Text text="Hi!" />
2666
- {page === 'A' ? (
2667
- <Text text="A" />
2668
- ) : (
2669
- <Suspense
2670
- fallback={<Text text={'Loading ' + page + '...'} />}
2671
- unstable_avoidThisFallback={true}>
2672
- <AsyncText text={page} ms={3000} />
2673
- </Suspense>
2674
- )}
2675
- </Suspense>
2676
- );
2677
- }
2678
-
2679
- // Initial render.
2680
- ReactNoop.render(<App page="A" />);
2681
- expect(Scheduler).toFlushAndYield(['Hi!', 'A']);
2682
- expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2447
+ // Later we load the data.
2448
+ Scheduler.unstable_advanceTime(5000);
2449
+ await advanceTimers(5000);
2450
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2451
+ expect(Scheduler).toFlushAndYield(['A']);
2452
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2453
2684
- // Start transition.
2454
+ // Start transition.
2455
+ await ReactNoop.act(async () => {
2456
React.unstable_withSuspenseConfig(
2686
- () => ReactNoop.render(<App page="B" />),
2687
- {timeoutMs: 2000},
2457
+ () => transitionToPage('B'),
2458
+ SUSPENSE_CONFIG,
2459
);
2460
2690
- expect(Scheduler).toFlushAndYield([
2691
- 'Hi!',
2692
- 'Suspend! [B]',
2693
- 'Loading B...',
2694
- ]);
2695
-
2696
- // Suspended
2697
- expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2698
- Scheduler.unstable_advanceTime(1800);
2699
- await advanceTimers(1800);
2700
- expect(Scheduler).toFlushAndYield([]);
2701
- // We should still be suspended here because this loading state should be avoided.
2702
- expect(ReactNoop.getChildren()).toEqual([span('Hi!'), span('A')]);
2703
- Scheduler.unstable_advanceTime(1500);
2704
- await advanceTimers(1500);
2705
- expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2461
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2462
+ Scheduler.unstable_advanceTime(1000);
2463
+ await advanceTimers(1000);
2464
+ // Even after a second, we have still not yet flushed the loading state.
2465
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2466
+ Scheduler.unstable_advanceTime(1100);
2467
+ await advanceTimers(1100);
2468
+ // After the timeout, we do show the loading state.
2469
expect(ReactNoop.getChildren()).toEqual([
2707
- span('Hi!'),
2708
- span('Loading B...'),
2470
+ hiddenSpan('A'),
2471
+ span('Loading...'),
2472
]);
2473
});
2474
+ // Later we load the data.
2475
+ Scheduler.unstable_advanceTime(3000);
2476
+ await advanceTimers(3000);
2477
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2478
+ expect(Scheduler).toFlushAndYield(['B']);
2479
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2480
+ });
2481
+ });
2482
2712
- it('supports delaying a busy spinner from disappearing', async () => {
2713
- function useLoadingIndicator(config) {
2714
- let [isLoading, setLoading] = React.useState(false);
2715
- let start = React.useCallback(
2716
- cb => {
2717
- setLoading(true);
2718
- Scheduler.unstable_next(() =>
2719
- React.unstable_withSuspenseConfig(() => {
2720
- setLoading(false);
2721
- cb();
2722
- }, config),
2723
- );
2724
- },
2725
- [setLoading, config],
2726
- );
2727
- return [isLoading, start];
2728
- }
2729
-
2730
- const SUSPENSE_CONFIG = {
2731
- timeoutMs: 10000,
2732
- busyDelayMs: 500,
2733
- busyMinDurationMs: 400,
2734
- };
2735
-
2736
- let transitionToPage;
2737
-
2738
- function App() {
2739
- let [page, setPage] = React.useState('A');
2740
- let [isLoading, startLoading] = useLoadingIndicator(SUSPENSE_CONFIG);
2741
- transitionToPage = nextPage => startLoading(() => setPage(nextPage));
2742
- return (
2743
- <Fragment>
2744
- <Text text={page} />
2745
- {isLoading ? <Text text="L" /> : null}
2746
- </Fragment>
2747
- );
2748
- }
2749
-
2750
- // Initial render.
2751
- ReactNoop.render(<App />);
2752
- expect(Scheduler).toFlushAndYield(['A']);
2753
- expect(ReactNoop.getChildren()).toEqual([span('A')]);
2483
+ it('disables suspense config when nothing is passed to withSuspenseConfig', async () => {
2484
+ function App({page}) {
2485
+ return (
2486
+ <Suspense fallback={<Text text="Loading..." />}>
2487
+ <AsyncText text={page} ms={2000} />
2488
+ </Suspense>
2489
+ );
2490
+ }
2491
+
2492
+ // Initial render.
2493
+ ReactNoop.render(<App page="A" />);
2494
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
This file is too large to show in full.