@samitouri / QOS-React / commits / 615df434bb

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.