main
js 370 lines 9.18 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import * as React from 'react';
11 import {
12 Fragment,
13 Suspense,
14 unstable_SuspenseList as SuspenseList,
15 useReducer,
16 useState,
17 } from 'react';
18
19 function SuspenseTree(): React.Node {
20 return (
21 <Fragment>
22 <h1>Suspense</h1>
23 <h4>Primary to Fallback Cycle</h4>
24 <PrimaryFallbackTest initialSuspend={false} />
25 <h4>Fallback to Primary Cycle</h4>
26 <PrimaryFallbackTest initialSuspend={true} />
27 <NestedSuspenseTest />
28 <SuspenseListTest />
29 <EmptySuspense />
30 <SuspenseTreeOperations />
31 </Fragment>
32 );
33 }
34
35 function IgnoreMePassthrough({children}: {children: React$Node}) {
36 return <span>{children}</span>;
37 }
38
39 const suspenseTreeOperationsChildren = {
40 a: (
41 <Suspense key="a" name="a">
42 <p>A</p>
43 </Suspense>
44 ),
45 b: (
46 <div key="b">
47 <Suspense name="b">B</Suspense>
48 </div>
49 ),
50 c: (
51 <p key="c">
52 <Suspense key="c" name="c">
53 C
54 </Suspense>
55 </p>
56 ),
57 d: (
58 <Suspense key="d" name="d">
59 <div>D</div>
60 </Suspense>
61 ),
62 e: (
63 <Suspense key="e" name="e">
64 <IgnoreMePassthrough key="e1">
65 <Suspense name="e-child-one">
66 <p>e1</p>
67 </Suspense>
68 </IgnoreMePassthrough>
69 <IgnoreMePassthrough key="e2">
70 <Suspense name="e-child-two">
71 <div>e2</div>
72 </Suspense>
73 </IgnoreMePassthrough>
74 </Suspense>
75 ),
76 eReordered: (
77 <Suspense key="e" name="e">
78 <IgnoreMePassthrough key="e2">
79 <Suspense name="e-child-two">
80 <div>e2</div>
81 </Suspense>
82 </IgnoreMePassthrough>
83 <IgnoreMePassthrough key="e1">
84 <Suspense name="e-child-one">
85 <p>e1</p>
86 </Suspense>
87 </IgnoreMePassthrough>
88 </Suspense>
89 ),
90 };
91
92 function SuspenseTreeOperations() {
93 const initialChildren: any[] = [
94 suspenseTreeOperationsChildren.a,
95 suspenseTreeOperationsChildren.b,
96 suspenseTreeOperationsChildren.c,
97 suspenseTreeOperationsChildren.d,
98 suspenseTreeOperationsChildren.e,
99 ];
100 const [children, dispatch] = useReducer(
101 (
102 pendingState: any[],
103 action: 'toggle-mount' | 'reorder' | 'reorder-within-filtered',
104 ): React$Node[] => {
105 switch (action) {
106 case 'toggle-mount':
107 if (pendingState.length === 5) {
108 return [
109 suspenseTreeOperationsChildren.a,
110 suspenseTreeOperationsChildren.b,
111 suspenseTreeOperationsChildren.c,
112 suspenseTreeOperationsChildren.d,
113 ];
114 } else {
115 return [
116 suspenseTreeOperationsChildren.a,
117 suspenseTreeOperationsChildren.b,
118 suspenseTreeOperationsChildren.c,
119 suspenseTreeOperationsChildren.d,
120 suspenseTreeOperationsChildren.e,
121 ];
122 }
123 case 'reorder':
124 if (pendingState[1] === suspenseTreeOperationsChildren.b) {
125 return [
126 suspenseTreeOperationsChildren.a,
127 suspenseTreeOperationsChildren.c,
128 suspenseTreeOperationsChildren.b,
129 suspenseTreeOperationsChildren.d,
130 suspenseTreeOperationsChildren.e,
131 ];
132 } else {
133 return [
134 suspenseTreeOperationsChildren.a,
135 suspenseTreeOperationsChildren.b,
136 suspenseTreeOperationsChildren.c,
137 suspenseTreeOperationsChildren.d,
138 suspenseTreeOperationsChildren.e,
139 ];
140 }
141 case 'reorder-within-filtered':
142 if (pendingState[4] === suspenseTreeOperationsChildren.e) {
143 return [
144 suspenseTreeOperationsChildren.a,
145 suspenseTreeOperationsChildren.b,
146 suspenseTreeOperationsChildren.c,
147 suspenseTreeOperationsChildren.d,
148 suspenseTreeOperationsChildren.eReordered,
149 ];
150 } else {
151 return [
152 suspenseTreeOperationsChildren.a,
153 suspenseTreeOperationsChildren.b,
154 suspenseTreeOperationsChildren.c,
155 suspenseTreeOperationsChildren.d,
156 suspenseTreeOperationsChildren.e,
157 ];
158 }
159 default:
160 return pendingState;
161 }
162 },
163 initialChildren,
164 );
165
166 return (
167 <>
168 <button onClick={() => dispatch('toggle-mount')}>Toggle Mount</button>
169 <button onClick={() => dispatch('reorder')}>Reorder</button>
170 <button onClick={() => dispatch('reorder-within-filtered')}>
171 Reorder Within Filtered
172 </button>
173 <Suspense name="operations-parent">
174 <section>{children}</section>
175 </Suspense>
176 </>
177 );
178 }
179
180 function EmptySuspense() {
181 return <Suspense />;
182 }
183
184 // $FlowFixMe[missing-local-annot]
185 function PrimaryFallbackTest({initialSuspend}) {
186 const [suspend, setSuspend] = useState(initialSuspend);
187 const [fallbackStepIndex, fallbackStep] = useTestSequence(
188 'fallback',
189 Fallback1,
190 Fallback2,
191 );
192 const [, primaryStep] = useTestSequence('primary', Primary1, Primary2);
193 return (
194 <Fragment>
195 <label>
196 <input
197 checked={suspend}
198 onChange={e => setSuspend(e.target.checked)}
199 type="checkbox"
200 />
201 Suspend
202 </label>
203 <br />
204 <Suspense fallback={fallbackStep}>
205 {suspend ? (
206 <Never id={`primary-fallback-test-${fallbackStepIndex}`} />
207 ) : (
208 primaryStep
209 )}
210 </Suspense>
211 </Fragment>
212 );
213 }
214
215 function useTestSequence(label: string, T1: any => any, T2: any => any) {
216 const [step, setStep] = useState(0);
217 const next: $FlowFixMe = (
218 <button onClick={() => setStep(s => (s + 1) % allSteps.length)}>
219 next {label} content
220 </button>
221 );
222 const allSteps: $FlowFixMe = [
223 <Fragment>{next}</Fragment>,
224 <Fragment>
225 {next} <T1 prop={step}>mount</T1>
226 </Fragment>,
227 <Fragment>
228 {next} <T1 prop={step}>update</T1>
229 </Fragment>,
230 <Fragment>
231 {next} <T2 prop={step}>several</T2> <T1 prop={step}>different</T1>{' '}
232 <T2 prop={step}>children</T2>
233 </Fragment>,
234 <Fragment>
235 {next} <T2 prop={step}>goodbye</T2>
236 </Fragment>,
237 ];
238 return [step, allSteps[step]];
239 }
240
241 function NestedSuspenseTest() {
242 return (
243 <Fragment>
244 <h3>Nested Suspense</h3>
245 <Suspense fallback={<Fallback1>Loading outer</Fallback1>}>
246 <Parent />
247 </Suspense>
248 </Fragment>
249 );
250 }
251
252 function Parent() {
253 return (
254 <div>
255 <Suspense fallback={<Fallback1>Loading inner 1</Fallback1>}>
256 <Primary1>Hello</Primary1>
257 </Suspense>{' '}
258 <Suspense fallback={<Fallback2>Loading inner 2</Fallback2>}>
259 <Primary2>World</Primary2>
260 </Suspense>
261 <br />
262 <Suspense fallback={<Fallback1>This will never load</Fallback1>}>
263 <Never id="parent-never" />
264 </Suspense>
265 <br />
266 <b>
267 <LoadLater />
268 </b>
269 </div>
270 );
271 }
272
273 function SuspenseListTest() {
274 return (
275 <>
276 <h1>SuspenseList</h1>
277 <SuspenseList revealOrder="forwards" tail="collapsed">
278 <div>
279 <Suspense fallback={<Fallback1>Loading 1</Fallback1>}>
280 <Primary1>Hello</Primary1>
281 </Suspense>
282 </div>
283 <div>
284 <LoadLater />
285 </div>
286 <div>
287 <Suspense fallback={<Fallback2>Loading 2</Fallback2>}>
288 <Primary2>World</Primary2>
289 </Suspense>
290 </div>
291 </SuspenseList>
292 </>
293 );
294 }
295
296 function LoadLater() {
297 const [loadChild, setLoadChild] = useState(false);
298 return (
299 <Suspense
300 fallback={
301 <Fallback1 onClick={() => setLoadChild(true)}>Click to load</Fallback1>
302 }
303 name="LoadLater">
304 {loadChild ? (
305 <Primary1 onClick={() => setLoadChild(false)}>
306 Loaded! Click to suspend again.
307 </Primary1>
308 ) : (
309 <Never id="load-later" />
310 )}
311 </Suspense>
312 );
313 }
314
315 function readRecord(promise: any): any {
316 if (typeof React.use === 'function') {
317 // eslint-disable-next-line react-hooks-published/rules-of-hooks
318 return React.use(promise);
319 }
320 switch (promise.status) {
321 case 'pending':
322 throw promise;
323 case 'rejected':
324 throw promise.reason;
325 case 'fulfilled':
326 return promise.value;
327 default:
328 promise.status = 'pending';
329 promise.then(
330 value => {
331 promise.status = 'fulfilled';
332 promise.value = value;
333 },
334 reason => {
335 promise.status = 'rejected';
336 promise.reason = reason;
337 },
338 );
339 throw promise;
340 }
341 }
342
343 const nevers = new Map<string, Promise<empty>>();
344 function Never({id}: {id: string}) {
345 let promise = nevers.get(id);
346 if (!promise) {
347 promise = new Promise(() => {});
348 (promise as any).displayName = id;
349 nevers.set(id, promise);
350 }
351 readRecord(promise);
352 }
353
354 function Fallback1({prop, ...rest}: any) {
355 return <span {...rest} />;
356 }
357
358 function Fallback2({prop, ...rest}: any) {
359 return <span {...rest} />;
360 }
361
362 function Primary1({prop, ...rest}: any) {
363 return <span {...rest} />;
364 }
365
366 function Primary2({prop, ...rest}: any) {
367 return <span {...rest} />;
368 }
369
370 export default SuspenseTree;