@@ -74,7 +74,11 @@ import {
74
74
cloneChildFibers ,
75
75
} from './ReactChildFiber' ;
76
76
import { processUpdateQueue } from './ReactUpdateQueue' ;
77
- import { NoWork , Never } from './ReactFiberExpirationTime' ;
77
+ import {
78
+ NoWork ,
79
+ Never ,
80
+ computeAsyncExpiration ,
81
+ } from './ReactFiberExpirationTime' ;
78
82
import {
79
83
ConcurrentMode ,
80
84
NoContext ,
@@ -133,7 +137,7 @@ import {
133
137
createWorkInProgress ,
134
138
isSimpleFunctionComponent ,
135
139
} from './ReactFiber' ;
136
- import { retryTimedOutBoundary } from './ReactFiberScheduler' ;
140
+ import { requestCurrentTime , retryTimedOutBoundary } from './ReactFiberScheduler' ;
137
141
138
142
const ReactCurrentOwner = ReactSharedInternals . ReactCurrentOwner ;
139
143
@@ -1631,15 +1635,71 @@ function updateSuspenseComponent(
1631
1635
return next ;
1632
1636
}
1633
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
+
1634
1674
function updateDehydratedSuspenseComponent (
1635
1675
current : Fiber | null ,
1636
1676
workInProgress : Fiber ,
1637
1677
renderExpirationTime : ExpirationTime ,
1638
1678
) {
1679
+ const suspenseInstance = ( workInProgress . stateNode : SuspenseInstance ) ;
1639
1680
if ( current === null ) {
1640
1681
// During the first pass, we'll bail out and not drill into the children.
1641
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
+ }
1643
1703
return null ;
1644
1704
}
1645
1705
if ( ( workInProgress . effectTag & DidCapture ) !== NoEffect ) {
@@ -1648,55 +1708,31 @@ function updateDehydratedSuspenseComponent(
1648
1708
workInProgress . child = null ;
1649
1709
return null ;
1650
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
+ }
1651
1721
// We use childExpirationTime to indicate that a child might depend on context, so if
1652
1722
// any context has changed, we need to treat is as if the input might have changed.
1653
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 ) {
1660
1725
// This boundary has changed since the first render. This means that we are now unable to
1661
1726
// hydrate it. We might still be able to hydrate it using an earlier expiration time but
1662
1727
// during this render we can't. Instead, we're going to delete the whole subtree and
1663
1728
// instead inject a new real Suspense boundary to take its place, which may render content
1664
1729
// or fallback. The real Suspense boundary will suspend for a while so we have some time
1665
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 ,
1681
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 ) ;
1700
1736
} else if ( isSuspenseInstancePending ( suspenseInstance ) ) {
1701
1737
// This component is still pending more data from the server, so we can't hydrate its
1702
1738
// content. We treat it as if this component suspended itself. It might seem as if
0 commit comments