@samitouri / QOS-React / commits / 8ced545e3d

Suspense component does not capture if `fallback` is not defined (#13879)

* Suspense component does not capture if `fallback` is not defined A missing fallback prop means the exception should propagate to the next parent (like a rethrow). That way a Suspense component can specify other props like maxDuration without needing to provide a fallback, too. Closes #13864 * Change order of checks

Andrew Clark committed Oct 18, 2018 at 16:07 UTC 8ced545e3df95afab6fa35bc29f9320bafbcef26
4 files changed +87 -29
packages/react-reconciler/src/ReactFiberUnwindWork.js
+3 -3
@@ -205,9 +205,9 @@ function throwException(
205 workInProgress = returnFiber;
206 do {
207 if (workInProgress.tag === SuspenseComponent) {
208 - const state = workInProgress.memoizedState;
209 - const didTimeout = state !== null && state.didTimeout;
210 - if (!didTimeout) {
208 + const fallback = workInProgress.memoizedProps.fallback;
209 + const didTimeout = workInProgress.memoizedState;
210 + if (!didTimeout && workInProgress.memoizedProps.fallback !== undefined) {
211 // Found the nearest boundary.
212
213 // If the boundary is not in concurrent mode, we should not suspend, and
packages/react-reconciler/src/__tests__/ReactPure-test.internal.js
+9 -9
@@ -57,11 +57,11 @@ describe('pure', () => {
57 Counter = pure(Counter);
58
59 ReactNoop.render(
60 - <Suspense>
60 + <Suspense fallback={<Text text="Loading..." />}>
61 <Counter count={0} />
62 </Suspense>,
63 );
64 - expect(ReactNoop.flush()).toEqual([]);
64 + expect(ReactNoop.flush()).toEqual(['Loading...']);
65 await Promise.resolve();
66 expect(ReactNoop.flush()).toEqual([0]);
67 expect(ReactNoop.getChildren()).toEqual([span(0)]);
@@ -107,7 +107,7 @@ describe('pure', () => {
107 state = {count: 0};
108 render() {
109 return (
110 - <Suspense>
110 + <Suspense fallback={<Text text="Loading..." />}>
111 <CountContext.Provider value={this.state.count}>
112 <Counter label="Count" />
113 </CountContext.Provider>
@@ -118,7 +118,7 @@ describe('pure', () => {
118
119 const parent = React.createRef(null);
120 ReactNoop.render(<Parent ref={parent} />);
121 - expect(ReactNoop.flush()).toEqual([]);
121 + expect(ReactNoop.flush()).toEqual(['Loading...']);
122 await Promise.resolve();
123 expect(ReactNoop.flush()).toEqual(['Count: 0']);
124 expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
@@ -148,11 +148,11 @@ describe('pure', () => {
148 });
149
150 ReactNoop.render(
151 - <Suspense>
151 + <Suspense fallback={<Text text="Loading..." />}>
152 <Counter count={0} />
153 </Suspense>,
154 );
155 - expect(ReactNoop.flush()).toEqual([]);
155 + expect(ReactNoop.flush()).toEqual(['Loading...']);
156 await Promise.resolve();
157 expect(ReactNoop.flush()).toEqual([0]);
158 expect(ReactNoop.getChildren()).toEqual([span(0)]);
@@ -204,7 +204,7 @@ describe('pure', () => {
204 const divRef = React.createRef();
205
206 ReactNoop.render(
207 - <Suspense>
207 + <Suspense fallback={<Text text="Loading..." />}>
208 <Transparent ref={divRef} />
209 </Suspense>,
210 );
@@ -224,11 +224,11 @@ describe('pure', () => {
224 const divRef2 = React.createRef();
225
226 ReactNoop.render(
227 - <Suspense>
227 + <Suspense fallback={<Text text="Loading..." />}>
228 <Transparent ref={divRef} />
229 </Suspense>,
230 );
231 - expect(ReactNoop.flush()).toEqual([]);
231 + expect(ReactNoop.flush()).toEqual(['Loading...']);
232 await Promise.resolve();
233 expect(ReactNoop.flush()).toEqual(['Text']);
234 expect(divRef.current.type).toBe('div');
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+47 -1
@@ -106,7 +106,7 @@ describe('ReactSuspense', () => {
106 function Foo() {
107 ReactTestRenderer.unstable_yield('Foo');
108 return (
109 - <Suspense>
109 + <Suspense fallback={<Text text="Loading..." />}>
110 <Bar>
111 <AsyncText text="A" ms={100} />
112 <Text text="B" />
@@ -126,6 +126,7 @@ describe('ReactSuspense', () => {
126 'Suspend! [A]',
127 // But we keep rendering the siblings
128 'B',
129 + 'Loading...',
130 ]);
131 expect(root).toMatchRenderedOutput(null);
132
@@ -271,4 +272,49 @@ describe('ReactSuspense', () => {
272 expect(ReactTestRenderer).toHaveYielded(['Hi', 'Did mount: Hi']);
273 expect(root).toMatchRenderedOutput('Hi');
274 });
275 +
276 + it('only captures if `fallback` is defined', () => {
277 + const root = ReactTestRenderer.create(
278 + <Suspense fallback={<Text text="Loading..." />}>
279 + <Suspense maxDuration={100}>
280 + <AsyncText text="Hi" ms={5000} />
281 + </Suspense>
282 + </Suspense>,
283 + {
284 + unstable_isConcurrent: true,
285 + },
286 + );
287 +
288 + expect(root).toFlushAndYield([
289 + 'Suspend! [Hi]',
290 + // The outer fallback should be rendered, because the inner one does not
291 + // have a `fallback` prop
292 + 'Loading...',
293 + ]);
294 + jest.advanceTimersByTime(1000);
295 + expect(ReactTestRenderer).toHaveYielded([]);
296 + expect(root).toFlushAndYield([]);
297 + expect(root).toMatchRenderedOutput('Loading...');
298 +
299 + jest.advanceTimersByTime(5000);
300 + expect(ReactTestRenderer).toHaveYielded(['Promise resolved [Hi]']);
301 + expect(root).toFlushAndYield(['Hi']);
302 + expect(root).toMatchRenderedOutput('Hi');
303 + });
304 +
305 + it('throws if tree suspends and none of the Suspense ancestors have a fallback', () => {
306 + const root = ReactTestRenderer.create(
307 + <Suspense>
308 + <AsyncText text="Hi" ms={1000} />
309 + </Suspense>,
310 + {
311 + unstable_isConcurrent: true,
312 + },
313 + );
314 +
315 + expect(root).toFlushAndThrow(
316 + 'An update was suspended, but no placeholder UI was provided.',
317 + );
318 + expect(ReactTestRenderer).toHaveYielded(['Suspend! [Hi]', 'Suspend! [Hi]']);
319 + });
320 });
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+28 -16
@@ -104,7 +104,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
104 function Foo() {
105 ReactNoop.yield('Foo');
106 return (
107 - <Suspense>
107 + <Suspense fallback={<Text text="Loading..." />}>
108 <Bar>
109 <AsyncText text="A" ms={100} />
110 <Text text="B" />
@@ -121,6 +121,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
121 'Suspend! [A]',
122 // But we keep rendering the siblings
123 'B',
124 + 'Loading...',
125 ]);
126 expect(ReactNoop.getChildren()).toEqual([]);
127
@@ -193,7 +194,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
194
195 it('continues rendering siblings after suspending', async () => {
196 ReactNoop.render(
196 - <Suspense>
197 + <Suspense fallback={<Text text="Loading..." />}>
198 <Text text="A" />
199 <AsyncText text="B" />
200 <Text text="C" />
@@ -201,7 +202,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
202 </Suspense>,
203 );
204 // B suspends. Continue rendering the remaining siblings.
204 - expect(ReactNoop.flush()).toEqual(['A', 'Suspend! [B]', 'C', 'D']);
205 + expect(ReactNoop.flush()).toEqual([
206 + 'A',
207 + 'Suspend! [B]',
208 + 'C',
209 + 'D',
210 + 'Loading...',
211 + ]);
212 // Did not commit yet.
213 expect(ReactNoop.getChildren()).toEqual([]);
214
@@ -243,7 +250,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
250 const errorBoundary = React.createRef();
251 function App() {
252 return (
246 - <Suspense>
253 + <Suspense fallback={<Text text="Loading..." />}>
254 <ErrorBoundary ref={errorBoundary}>
255 <AsyncText text="Result" ms={1000} />
256 </ErrorBoundary>
@@ -252,7 +259,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
259 }
260
261 ReactNoop.render(<App />);
255 - expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']);
262 + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']);
263 expect(ReactNoop.getChildren()).toEqual([]);
264
265 textResourceShouldFail = true;
@@ -278,7 +285,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
285 errorBoundary.current.reset();
286 cache.invalidate();
287
281 - expect(ReactNoop.flush()).toEqual(['Suspend! [Result]']);
288 + expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']);
289 ReactNoop.expire(1000);
290 await advanceTimers(1000);
291 expect(ReactNoop.flush()).toEqual(['Promise resolved [Result]', 'Result']);
@@ -356,7 +363,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
363 it('can update at a higher priority while in a suspended state', async () => {
364 function App(props) {
365 return (
359 - <Suspense>
366 + <Suspense fallback={<Text text="Loading..." />}>
367 <Text text={props.highPri} />
368 <AsyncText text={props.lowPri} />
369 </Suspense>
@@ -376,6 +383,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
383 'A',
384 // Suspends
385 'Suspend! [2]',
386 + 'Loading...',
387 ]);
388
389 // While we're still waiting for the low-pri update to complete, update the
@@ -395,7 +403,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
403 it('keeps working on lower priority work after being pinged', async () => {
404 function App(props) {
405 return (
398 - <Suspense>
406 + <Suspense fallback={<Text text="Loading..." />}>
407 <AsyncText text="A" />
408 {props.showB && <Text text="B" />}
409 </Suspense>
@@ -403,13 +411,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
411 }
412
413 ReactNoop.render(<App showB={false} />);
406 - expect(ReactNoop.flush()).toEqual(['Suspend! [A]']);
414 + expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'Loading...']);
415 expect(ReactNoop.getChildren()).toEqual([]);
416
417 // Advance React's virtual time by enough to fall into a new async bucket.
418 ReactNoop.expire(1200);
419 ReactNoop.render(<App showB={true} />);
412 - expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'B']);
420 + expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'B', 'Loading...']);
421 expect(ReactNoop.getChildren()).toEqual([]);
422
423 await advanceTimers(0);
@@ -676,13 +684,17 @@ describe('ReactSuspenseWithNoopRenderer', () => {
684
685 it('a Suspense component correctly handles more than one suspended child', async () => {
686 ReactNoop.render(
679 - <Suspense maxDuration={0}>
687 + <Suspense maxDuration={0} fallback={<Text text="Loading..." />}>
688 <AsyncText text="A" ms={100} />
689 <AsyncText text="B" ms={100} />
690 </Suspense>,
691 );
684 - expect(ReactNoop.expire(10000)).toEqual(['Suspend! [A]', 'Suspend! [B]']);
685 - expect(ReactNoop.getChildren()).toEqual([]);
692 + expect(ReactNoop.expire(10000)).toEqual([
693 + 'Suspend! [A]',
694 + 'Suspend! [B]',
695 + 'Loading...',
696 + ]);
697 + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
698
699 await advanceTimers(100);
700
@@ -722,7 +734,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
734 it('starts working on an update even if its priority falls between two suspended levels', async () => {
735 function App(props) {
736 return (
725 - <Suspense maxDuration={10000}>
737 + <Suspense fallback={<Text text="Loading..." />} maxDuration={10000}>
738 {props.text === 'C' ? (
739 <Text text="C" />
740 ) : (
@@ -735,7 +747,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
747 // Schedule an update
748 ReactNoop.render(<App text="A" />);
749 // The update should suspend.
738 - expect(ReactNoop.flush()).toEqual(['Suspend! [A]']);
750 + expect(ReactNoop.flush()).toEqual(['Suspend! [A]', 'Loading...']);
751 expect(ReactNoop.getChildren()).toEqual([]);
752
753 // Advance time until right before it expires. This number may need to
@@ -748,7 +760,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
760 // Schedule another low priority update.
761 ReactNoop.render(<App text="B" />);
762 // This update should also suspend.
751 - expect(ReactNoop.flush()).toEqual(['Suspend! [B]']);
763 + expect(ReactNoop.flush()).toEqual(['Suspend! [B]', 'Loading...']);
764 expect(ReactNoop.getChildren()).toEqual([]);
765
766 // Schedule a high priority update. Its expiration time will fall between