| 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; |