74
cloneChildFibers,
75
} from './ReactChildFiber';
76
import {processUpdateQueue} from './ReactUpdateQueue';
77
-import {NoWork, Never} from './ReactFiberExpirationTime';
77
+import {
78
+ NoWork,
79
+ Never,
80
+ computeAsyncExpiration,
81
+} from './ReactFiberExpirationTime';
82
import {
83
ConcurrentMode,
84
NoContext,
137
createWorkInProgress,
138
isSimpleFunctionComponent,
139
} from './ReactFiber';
136
-import {retryTimedOutBoundary} from './ReactFiberScheduler';
140
+import {requestCurrentTime, retryTimedOutBoundary} from './ReactFiberScheduler';
141
142
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
143
1635
return next;
1636
}
1637
1638
+function retrySuspenseComponentWithoutHydrating(
1639
+ current: Fiber,
1640
+ workInProgress: Fiber,
1641
+ renderExpirationTime: ExpirationTime,
1642
+) {
1643
+ // Detach from the current dehydrated boundary.
1644
+ current.alternate = null;
1645
+ workInProgress.alternate = null;
1646
+
1647
+ // Insert a deletion in the effect list.
1648
+ let returnFiber = workInProgress.return;
1649
+ invariant(
1650
+ returnFiber !== null,
1651
+ 'Suspense boundaries are never on the root. ' +
1652
+ 'This is probably a bug in React.',
1653
+ );
1654
+ const last = returnFiber.lastEffect;
1655
+ if (last !== null) {
1656
+ last.nextEffect = current;
1657
+ returnFiber.lastEffect = current;
1658
+ } else {
1659
+ returnFiber.firstEffect = returnFiber.lastEffect = current;
1660
+ }
1661
+ current.nextEffect = null;
1662
+ current.effectTag = Deletion;
1663
+
1664
+ // Upgrade this work in progress to a real Suspense component.
1665
+ workInProgress.tag = SuspenseComponent;
1666
+ workInProgress.stateNode = null;
1667
+ workInProgress.memoizedState = null;
1668
+ // This is now an insertion.
1669
+ workInProgress.effectTag |= Placement;
1670
+ // Retry as a real Suspense component.
1671
+ return updateSuspenseComponent(null, workInProgress, renderExpirationTime);
1672
+}
1673
+
1674
function updateDehydratedSuspenseComponent(
1675
current: Fiber | null,
1676
workInProgress: Fiber,
1677
renderExpirationTime: ExpirationTime,
1678
) {
1679
+ const suspenseInstance = (workInProgress.stateNode: SuspenseInstance);
1680
if (current === null) {
1681
// During the first pass, we'll bail out and not drill into the children.
1682
// Instead, we'll leave the content in place and try to hydrate it later.
1642
- workInProgress.expirationTime = Never;
1683
+ if (isSuspenseInstanceFallback(suspenseInstance)) {
1684
+ // This is a client-only boundary. Since we won't get any content from the server
1685
+ // for this, we need to schedule that at a higher priority based on when it would
1686
+ // have timed out. In theory we could render it in this pass but it would have the
1687
+ // wrong priority associated with it and will prevent hydration of parent path.
1688
+ // Instead, we'll leave work left on it to render it in a separate commit.
1689
+
1690
+ // TODO This time should be the time at which the server rendered response that is
1691
+ // a parent to this boundary was displayed. However, since we currently don't have
1692
+ // a protocol to transfer that time, we'll just estimate it by using the current
1693
+ // time. This will mean that Suspense timeouts are slightly shifted to later than
1694
+ // they should be.
1695
+ let serverDisplayTime = requestCurrentTime();
1696
+ // Schedule a normal pri update to render this content.
1697
+ workInProgress.expirationTime = computeAsyncExpiration(serverDisplayTime);
1698
+ } else {
1699
+ // We'll continue hydrating the rest at offscreen priority since we'll already
1700
+ // be showing the right content coming from the server, it is no rush.
1701
+ workInProgress.expirationTime = Never;
1702
+ }
1703
return null;
1704
}
1705
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
1708
workInProgress.child = null;
1709
return null;
1710
}
1711
+ if (isSuspenseInstanceFallback(suspenseInstance)) {
1712
+ // This boundary is in a permanent fallback state. In this case, we'll never
1713
+ // get an update and we'll never be able to hydrate the final content. Let's just try the
1714
+ // client side render instead.
1715
+ return retrySuspenseComponentWithoutHydrating(
1716
+ current,
1717
+ workInProgress,
1718
+ renderExpirationTime,
1719
+ );
1720
+ }
1721
// We use childExpirationTime to indicate that a child might depend on context, so if
1722
// any context has changed, we need to treat is as if the input might have changed.
1723
const hasContextChanged = current.childExpirationTime >= renderExpirationTime;
1654
- const suspenseInstance = (current.stateNode: SuspenseInstance);
1655
- if (
1656
- didReceiveUpdate ||
1657
- hasContextChanged ||
1658
- isSuspenseInstanceFallback(suspenseInstance)
1659
- ) {
1724
+ if (didReceiveUpdate || hasContextChanged) {
1725
// This boundary has changed since the first render. This means that we are now unable to
1726
// hydrate it. We might still be able to hydrate it using an earlier expiration time but
1727
// during this render we can't. Instead, we're going to delete the whole subtree and
1728
// instead inject a new real Suspense boundary to take its place, which may render content
1729
// or fallback. The real Suspense boundary will suspend for a while so we have some time
1730
// to ensure it can produce real content, but all state and pending events will be lost.
1666
-
1667
- // Alternatively, this boundary is in a permanent fallback state. In this case, we'll never
1668
- // get an update and we'll never be able to hydrate the final content. Let's just try the
1669
- // client side render instead.
1670
-
1671
- // Detach from the current dehydrated boundary.
1672
- current.alternate = null;
1673
- workInProgress.alternate = null;
1674
-
1675
- // Insert a deletion in the effect list.
1676
- let returnFiber = workInProgress.return;
1677
- invariant(
1678
- returnFiber !== null,
1679
- 'Suspense boundaries are never on the root. ' +
1680
- 'This is probably a bug in React.',
1731
+ return retrySuspenseComponentWithoutHydrating(
1732
+ current,
1733
+ workInProgress,
1734
+ renderExpirationTime,
1735
);
1682
- const last = returnFiber.lastEffect;
1683
- if (last !== null) {
1684
- last.nextEffect = current;
1685
- returnFiber.lastEffect = current;
1686
- } else {
1687
- returnFiber.firstEffect = returnFiber.lastEffect = current;
1688
- }
1689
- current.nextEffect = null;
1690
- current.effectTag = Deletion;
1691
-
1692
- // Upgrade this work in progress to a real Suspense component.
1693
- workInProgress.tag = SuspenseComponent;
1694
- workInProgress.stateNode = null;
1695
- workInProgress.memoizedState = null;
1696
- // This is now an insertion.
1697
- workInProgress.effectTag |= Placement;
1698
- // Retry as a real Suspense component.
1699
- return updateSuspenseComponent(null, workInProgress, renderExpirationTime);
1736
} else if (isSuspenseInstancePending(suspenseInstance)) {
1737
// This component is still pending more data from the server, so we can't hydrate its
1738
// content. We treat it as if this component suspended itself. It might seem as if