@@ -74,7 +74,11 @@ import {
7474 cloneChildFibers ,
7575} from './ReactChildFiber' ;
7676import { processUpdateQueue } from './ReactUpdateQueue' ;
77- import { NoWork , Never } from './ReactFiberExpirationTime' ;
77+ import {
78+ NoWork ,
79+ Never ,
80+ computeAsyncExpiration ,
81+ } from './ReactFiberExpirationTime' ;
7882import {
7983 ConcurrentMode ,
8084 NoContext ,
@@ -133,7 +137,7 @@ import {
133137 createWorkInProgress ,
134138 isSimpleFunctionComponent ,
135139} from './ReactFiber' ;
136- import { retryTimedOutBoundary } from './ReactFiberScheduler' ;
140+ import { requestCurrentTime , retryTimedOutBoundary } from './ReactFiberScheduler' ;
137141
138142const ReactCurrentOwner = ReactSharedInternals . ReactCurrentOwner ;
139143
@@ -1631,15 +1635,71 @@ function updateSuspenseComponent(
16311635 return next ;
16321636}
16331637
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+
16341674function updateDehydratedSuspenseComponent (
16351675 current : Fiber | null ,
16361676 workInProgress : Fiber ,
16371677 renderExpirationTime : ExpirationTime ,
16381678) {
1679+ const suspenseInstance = ( workInProgress . stateNode : SuspenseInstance ) ;
16391680 if ( current === null ) {
16401681 // During the first pass, we'll bail out and not drill into the children.
16411682 // 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+ }
16431703 return null ;
16441704 }
16451705 if ( ( workInProgress . effectTag & DidCapture ) !== NoEffect ) {
@@ -1648,55 +1708,31 @@ function updateDehydratedSuspenseComponent(
16481708 workInProgress . child = null ;
16491709 return null ;
16501710 }
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+ }
16511721 // We use childExpirationTime to indicate that a child might depend on context, so if
16521722 // any context has changed, we need to treat is as if the input might have changed.
16531723 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 ) {
16601725 // This boundary has changed since the first render. This means that we are now unable to
16611726 // hydrate it. We might still be able to hydrate it using an earlier expiration time but
16621727 // during this render we can't. Instead, we're going to delete the whole subtree and
16631728 // instead inject a new real Suspense boundary to take its place, which may render content
16641729 // or fallback. The real Suspense boundary will suspend for a while so we have some time
16651730 // 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 ,
16811735 ) ;
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 ) ;
17001736 } else if ( isSuspenseInstancePending ( suspenseInstance ) ) {
17011737 // This component is still pending more data from the server, so we can't hydrate its
17021738 // content. We treat it as if this component suspended itself. It might seem as if
0 commit comments