41
enableDebugTracing,
42
enableLazyContextPropagation,
43
enableUpdaterTracking,
44
+ enablePostpone,
45
} from 'shared/ReactFeatureFlags';
46
import {createCapturedValueAtFiber} from './ReactCapturedValue';
47
import {
83
} from './ReactFiberHydrationContext';
84
import {ConcurrentRoot} from './ReactRootTags';
85
import {noopSuspenseyCommitThenable} from './ReactFiberThenable';
86
+import {REACT_POSTPONE_TYPE} from 'shared/ReactSymbols';
87
88
function createRootErrorUpdate(
89
fiber: Fiber,
334
}
335
}
336
335
- if (
336
- value !== null &&
337
- typeof value === 'object' &&
338
- typeof value.then === 'function'
339
- ) {
340
- // This is a wakeable. The component suspended.
341
- const wakeable: Wakeable = (value: any);
342
- resetSuspendedComponent(sourceFiber, rootRenderLanes);
337
+ if (value !== null && typeof value === 'object') {
338
+ if (enablePostpone && value.$$typeof === REACT_POSTPONE_TYPE) {
339
+ // Act as if this is an infinitely suspending promise.
340
+ value = {then: function () {}};
341
+ }
342
+ if (typeof value.then === 'function') {
343
+ // This is a wakeable. The component suspended.
344
+ const wakeable: Wakeable = (value: any);
345
+ resetSuspendedComponent(sourceFiber, rootRenderLanes);
346
344
- if (__DEV__) {
345
- if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
346
- markDidThrowWhileHydratingDEV();
347
+ if (__DEV__) {
348
+ if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
349
+ markDidThrowWhileHydratingDEV();
350
+ }
351
}
348
- }
352
350
- if (__DEV__) {
351
- if (enableDebugTracing) {
352
- if (sourceFiber.mode & DebugTracingMode) {
353
- const name = getComponentNameFromFiber(sourceFiber) || 'Unknown';
354
- logComponentSuspended(name, wakeable);
353
+ if (__DEV__) {
354
+ if (enableDebugTracing) {
355
+ if (sourceFiber.mode & DebugTracingMode) {
356
+ const name = getComponentNameFromFiber(sourceFiber) || 'Unknown';
357
+ logComponentSuspended(name, wakeable);
358
+ }
359
}
360
}
357
- }
361
359
- // Mark the nearest Suspense boundary to switch to rendering a fallback.
360
- const suspenseBoundary = getSuspenseHandler();
361
- if (suspenseBoundary !== null) {
362
- switch (suspenseBoundary.tag) {
363
- case SuspenseComponent: {
364
- // If this suspense boundary is not already showing a fallback, mark
365
- // the in-progress render as suspended. We try to perform this logic
366
- // as soon as soon as possible during the render phase, so the work
367
- // loop can know things like whether it's OK to switch to other tasks,
368
- // or whether it can wait for data to resolve before continuing.
369
- // TODO: Most of these checks are already performed when entering a
370
- // Suspense boundary. We should track the information on the stack so
371
- // we don't have to recompute it on demand. This would also allow us
372
- // to unify with `use` which needs to perform this logic even sooner,
373
- // before `throwException` is called.
374
- if (sourceFiber.mode & ConcurrentMode) {
375
- if (getShellBoundary() === null) {
376
- // Suspended in the "shell" of the app. This is an undesirable
377
- // loading state. We should avoid committing this tree.
378
- renderDidSuspendDelayIfPossible();
379
- } else {
380
- // If we suspended deeper than the shell, we don't need to delay
381
- // the commmit. However, we still call renderDidSuspend if this is
382
- // a new boundary, to tell the work loop that a new fallback has
383
- // appeared during this render.
384
- // TODO: Theoretically we should be able to delete this branch.
385
- // It's currently used for two things: 1) to throttle the
386
- // appearance of successive loading states, and 2) in
387
- // SuspenseList, to determine whether the children include any
388
- // pending fallbacks. For 1, we should apply throttling to all
389
- // retries, not just ones that render an additional fallback. For
390
- // 2, we should check subtreeFlags instead. Then we can delete
391
- // this branch.
392
- const current = suspenseBoundary.alternate;
393
- if (current === null) {
394
- renderDidSuspend();
362
+ // Mark the nearest Suspense boundary to switch to rendering a fallback.
363
+ const suspenseBoundary = getSuspenseHandler();
364
+ if (suspenseBoundary !== null) {
365
+ switch (suspenseBoundary.tag) {
366
+ case SuspenseComponent: {
367
+ // If this suspense boundary is not already showing a fallback, mark
368
+ // the in-progress render as suspended. We try to perform this logic
369
+ // as soon as soon as possible during the render phase, so the work
370
+ // loop can know things like whether it's OK to switch to other tasks,
371
+ // or whether it can wait for data to resolve before continuing.
372
+ // TODO: Most of these checks are already performed when entering a
373
+ // Suspense boundary. We should track the information on the stack so
374
+ // we don't have to recompute it on demand. This would also allow us
375
+ // to unify with `use` which needs to perform this logic even sooner,
376
+ // before `throwException` is called.
377
+ if (sourceFiber.mode & ConcurrentMode) {
378
+ if (getShellBoundary() === null) {
379
+ // Suspended in the "shell" of the app. This is an undesirable
380
+ // loading state. We should avoid committing this tree.
381
+ renderDidSuspendDelayIfPossible();
382
+ } else {
383
+ // If we suspended deeper than the shell, we don't need to delay
384
+ // the commmit. However, we still call renderDidSuspend if this is
385
+ // a new boundary, to tell the work loop that a new fallback has
386
+ // appeared during this render.
387
+ // TODO: Theoretically we should be able to delete this branch.
388
+ // It's currently used for two things: 1) to throttle the
389
+ // appearance of successive loading states, and 2) in
390
+ // SuspenseList, to determine whether the children include any
391
+ // pending fallbacks. For 1, we should apply throttling to all
392
+ // retries, not just ones that render an additional fallback. For
393
+ // 2, we should check subtreeFlags instead. Then we can delete
394
+ // this branch.
395
+ const current = suspenseBoundary.alternate;
396
+ if (current === null) {
397
+ renderDidSuspend();
398
+ }
399
}
400
}
397
- }
401
399
- suspenseBoundary.flags &= ~ForceClientRender;
400
- markSuspenseBoundaryShouldCapture(
401
- suspenseBoundary,
402
- returnFiber,
403
- sourceFiber,
404
- root,
405
- rootRenderLanes,
406
- );
407
- // Retry listener
408
- //
409
- // If the fallback does commit, we need to attach a different type of
410
- // listener. This one schedules an update on the Suspense boundary to
411
- // turn the fallback state off.
412
- //
413
- // Stash the wakeable on the boundary fiber so we can access it in the
414
- // commit phase.
415
- //
416
- // When the wakeable resolves, we'll attempt to render the boundary
417
- // again ("retry").
402
+ suspenseBoundary.flags &= ~ForceClientRender;
403
+ markSuspenseBoundaryShouldCapture(
404
+ suspenseBoundary,
405
+ returnFiber,
406
+ sourceFiber,
407
+ root,
408
+ rootRenderLanes,
409
+ );
410
+ // Retry listener
411
+ //
412
+ // If the fallback does commit, we need to attach a different type of
413
+ // listener. This one schedules an update on the Suspense boundary to
414
+ // turn the fallback state off.
415
+ //
416
+ // Stash the wakeable on the boundary fiber so we can access it in the
417
+ // commit phase.
418
+ //
419
+ // When the wakeable resolves, we'll attempt to render the boundary
420
+ // again ("retry").
421
419
- // Check if this is a Suspensey resource. We do not attach retry
420
- // listeners to these, because we don't actually need them for
421
- // rendering. Only for committing. Instead, if a fallback commits
422
- // and the only thing that suspended was a Suspensey resource, we
423
- // retry immediately.
424
- // TODO: Refactor throwException so that we don't have to do this type
425
- // check. The caller already knows what the cause was.
426
- const isSuspenseyResource = wakeable === noopSuspenseyCommitThenable;
427
- if (isSuspenseyResource) {
428
- suspenseBoundary.flags |= ScheduleRetry;
429
- } else {
430
- const retryQueue: RetryQueue | null =
431
- (suspenseBoundary.updateQueue: any);
432
- if (retryQueue === null) {
433
- suspenseBoundary.updateQueue = new Set([wakeable]);
434
- } else {
435
- retryQueue.add(wakeable);
436
- }
437
- }
438
- break;
439
- }
440
- case OffscreenComponent: {
441
- if (suspenseBoundary.mode & ConcurrentMode) {
442
- suspenseBoundary.flags |= ShouldCapture;
422
+ // Check if this is a Suspensey resource. We do not attach retry
423
+ // listeners to these, because we don't actually need them for
424
+ // rendering. Only for committing. Instead, if a fallback commits
425
+ // and the only thing that suspended was a Suspensey resource, we
426
+ // retry immediately.
427
+ // TODO: Refactor throwException so that we don't have to do this type
428
+ // check. The caller already knows what the cause was.
429
const isSuspenseyResource =
430
wakeable === noopSuspenseyCommitThenable;
431
if (isSuspenseyResource) {
432
suspenseBoundary.flags |= ScheduleRetry;
433
} else {
448
- const offscreenQueue: OffscreenQueue | null =
434
+ const retryQueue: RetryQueue | null =
435
(suspenseBoundary.updateQueue: any);
450
- if (offscreenQueue === null) {
451
- const newOffscreenQueue: OffscreenQueue = {
452
- transitions: null,
453
- markerInstances: null,
454
- retryQueue: new Set([wakeable]),
455
- };
456
- suspenseBoundary.updateQueue = newOffscreenQueue;
436
+ if (retryQueue === null) {
437
+ suspenseBoundary.updateQueue = new Set([wakeable]);
438
+ } else {
439
+ retryQueue.add(wakeable);
440
+ }
441
+ }
442
+ break;
443
+ }
444
+ case OffscreenComponent: {
445
+ if (suspenseBoundary.mode & ConcurrentMode) {
446
+ suspenseBoundary.flags |= ShouldCapture;
447
+ const isSuspenseyResource =
448
+ wakeable === noopSuspenseyCommitThenable;
449
+ if (isSuspenseyResource) {
450
+ suspenseBoundary.flags |= ScheduleRetry;
451
} else {
458
- const retryQueue = offscreenQueue.retryQueue;
459
- if (retryQueue === null) {
460
- offscreenQueue.retryQueue = new Set([wakeable]);
452
+ const offscreenQueue: OffscreenQueue | null =
453
+ (suspenseBoundary.updateQueue: any);
454
+ if (offscreenQueue === null) {
455
+ const newOffscreenQueue: OffscreenQueue = {
456
+ transitions: null,
457
+ markerInstances: null,
458
+ retryQueue: new Set([wakeable]),
459
+ };
460
+ suspenseBoundary.updateQueue = newOffscreenQueue;
461
} else {
462
- retryQueue.add(wakeable);
462
+ const retryQueue = offscreenQueue.retryQueue;
463
+ if (retryQueue === null) {
464
+ offscreenQueue.retryQueue = new Set([wakeable]);
465
+ } else {
466
+ retryQueue.add(wakeable);
467
+ }
468
}
469
}
470
+ break;
471
}
466
- break;
472
+ // Fall through
473
+ }
474
+ default: {
475
+ throw new Error(
476
+ `Unexpected Suspense handler tag (${suspenseBoundary.tag}). This ` +
477
+ 'is a bug in React.',
478
+ );
479
}
468
- // Fall through
480
}
470
- default: {
471
- throw new Error(
472
- `Unexpected Suspense handler tag (${suspenseBoundary.tag}). This ` +
473
- 'is a bug in React.',
474
- );
481
+ // We only attach ping listeners in concurrent mode. Legacy Suspense always
482
+ // commits fallbacks synchronously, so there are no pings.
483
+ if (suspenseBoundary.mode & ConcurrentMode) {
484
+ attachPingListener(root, wakeable, rootRenderLanes);
485
}
476
- }
477
- // We only attach ping listeners in concurrent mode. Legacy Suspense always
478
- // commits fallbacks synchronously, so there are no pings.
479
- if (suspenseBoundary.mode & ConcurrentMode) {
480
- attachPingListener(root, wakeable, rootRenderLanes);
481
- }
482
- return;
483
- } else {
484
- // No boundary was found. Unless this is a sync update, this is OK.
485
- // We can suspend and wait for more data to arrive.
486
-
487
- if (root.tag === ConcurrentRoot) {
488
- // In a concurrent root, suspending without a Suspense boundary is
489
- // allowed. It will suspend indefinitely without committing.
490
- //
491
- // TODO: Should we have different behavior for discrete updates? What
492
- // about flushSync? Maybe it should put the tree into an inert state,
493
- // and potentially log a warning. Revisit this for a future release.
494
- attachPingListener(root, wakeable, rootRenderLanes);
495
- renderDidSuspendDelayIfPossible();
486
return;
487
} else {
498
- // In a legacy root, suspending without a boundary is always an error.
499
- const uncaughtSuspenseError = new Error(
500
- 'A component suspended while responding to synchronous input. This ' +
501
- 'will cause the UI to be replaced with a loading indicator. To ' +
502
- 'fix, updates that suspend should be wrapped ' +
503
- 'with startTransition.',
504
- );
505
- value = uncaughtSuspenseError;
488
+ // No boundary was found. Unless this is a sync update, this is OK.
489
+ // We can suspend and wait for more data to arrive.
490
+
491
+ if (root.tag === ConcurrentRoot) {
492
+ // In a concurrent root, suspending without a Suspense boundary is
493
+ // allowed. It will suspend indefinitely without committing.
494
+ //
495
+ // TODO: Should we have different behavior for discrete updates? What
496
+ // about flushSync? Maybe it should put the tree into an inert state,
497
+ // and potentially log a warning. Revisit this for a future release.
498
+ attachPingListener(root, wakeable, rootRenderLanes);
499
+ renderDidSuspendDelayIfPossible();
500
+ return;
501
+ } else {
502
+ // In a legacy root, suspending without a boundary is always an error.
503
+ const uncaughtSuspenseError = new Error(
504
+ 'A component suspended while responding to synchronous input. This ' +
505
+ 'will cause the UI to be replaced with a loading indicator. To ' +
506
+ 'fix, updates that suspend should be wrapped ' +
507
+ 'with startTransition.',
508
+ );
509
+ value = uncaughtSuspenseError;
510
+ }
511
}
512
}
508
- } else {
509
- // This is a regular error, not a Suspense wakeable.
510
- if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
511
- markDidThrowWhileHydratingDEV();
512
- const suspenseBoundary = getSuspenseHandler();
513
- // If the error was thrown during hydration, we may be able to recover by
514
- // discarding the dehydrated content and switching to a client render.
515
- // Instead of surfacing the error, find the nearest Suspense boundary
516
- // and render it again without hydration.
517
- if (suspenseBoundary !== null) {
518
- if ((suspenseBoundary.flags & ShouldCapture) === NoFlags) {
519
- // Set a flag to indicate that we should try rendering the normal
520
- // children again, not the fallback.
521
- suspenseBoundary.flags |= ForceClientRender;
522
- }
523
- markSuspenseBoundaryShouldCapture(
524
- suspenseBoundary,
525
- returnFiber,
526
- sourceFiber,
527
- root,
528
- rootRenderLanes,
529
- );
513
+ }
514
531
- // Even though the user may not be affected by this error, we should
532
- // still log it so it can be fixed.
533
- queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
534
- return;
515
+ // This is a regular error, not a Suspense wakeable.
516
+ if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
517
+ markDidThrowWhileHydratingDEV();
518
+ const suspenseBoundary = getSuspenseHandler();
519
+ // If the error was thrown during hydration, we may be able to recover by
520
+ // discarding the dehydrated content and switching to a client render.
521
+ // Instead of surfacing the error, find the nearest Suspense boundary
522
+ // and render it again without hydration.
523
+ if (suspenseBoundary !== null) {
524
+ if ((suspenseBoundary.flags & ShouldCapture) === NoFlags) {
525
+ // Set a flag to indicate that we should try rendering the normal
526
+ // children again, not the fallback.
527
+ suspenseBoundary.flags |= ForceClientRender;
528
}
536
- } else {
537
- // Otherwise, fall through to the error path.
529
+ markSuspenseBoundaryShouldCapture(
530
+ suspenseBoundary,
531
+ returnFiber,
532
+ sourceFiber,
533
+ root,
534
+ rootRenderLanes,
535
+ );
536
+
537
+ // Even though the user may not be affected by this error, we should
538
+ // still log it so it can be fixed.
539
+ queueHydrationError(createCapturedValueAtFiber(value, sourceFiber));
540
+ return;
541
}
542
+ } else {
543
+ // Otherwise, fall through to the error path.
544
}
545
546
value = createCapturedValueAtFiber(value, sourceFiber);