36
Profiler,
37
SuspenseComponent,
38
SuspenseListComponent,
39
- DehydratedSuspenseComponent,
39
MemoComponent,
40
SimpleMemoComponent,
41
LazyComponent,
92
import {
93
NoWork,
94
Never,
95
+ Sync,
96
computeAsyncExpiration,
97
} from './ReactFiberExpirationTime';
98
import {
115
import {
116
suspenseStackCursor,
117
pushSuspenseContext,
118
- popSuspenseContext,
118
InvisibleParentSuspenseContext,
119
ForceSuspenseFallback,
120
hasSuspenseContext,
1474
}
1475
}
1476
1478
-// TODO: This is now an empty object. Should we just make it a boolean?
1479
-const SUSPENDED_MARKER: SuspenseState = ({}: any);
1477
+const SUSPENDED_MARKER: SuspenseState = {
1478
+ dehydrated: null,
1479
+};
1480
1481
function shouldRemainOnFallback(
1482
suspenseContext: SuspenseContext,
1511
1512
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
1513
1514
- let nextState = null;
1514
let nextDidTimeout = false;
1515
+ const didSuspend = (workInProgress.effectTag & DidCapture) !== NoEffect;
1516
1517
if (
1518
- (workInProgress.effectTag & DidCapture) !== NoEffect ||
1518
+ didSuspend ||
1519
shouldRemainOnFallback(suspenseContext, current, workInProgress)
1520
) {
1521
// Something in this boundary's subtree already suspended. Switch to
1522
// rendering the fallback children.
1523
- nextState = SUSPENDED_MARKER;
1523
nextDidTimeout = true;
1524
workInProgress.effectTag &= ~DidCapture;
1525
} else {
1526
// Attempting the main content
1528
- if (current === null || current.memoizedState !== null) {
1527
+ if (
1528
+ current === null ||
1529
+ (current.memoizedState: null | SuspenseState) !== null
1530
+ ) {
1531
// This is a new mount or this boundary is already showing a fallback state.
1532
// Mark this subtree context as having at least one invisible parent that could
1533
// handle the fallback state.
1584
// custom reconciliation logic to preserve the state of the primary
1585
// children. It's essentially a very basic form of re-parenting.
1586
1585
- // `child` points to the child fiber. In the normal case, this is the first
1586
- // fiber of the primary children set. In the timed-out case, it's a
1587
- // a fragment fiber containing the primary children.
1588
- let child;
1589
- // `next` points to the next fiber React should render. In the normal case,
1590
- // it's the same as `child`: the first fiber of the primary children set.
1591
- // In the timed-out case, it's a fragment fiber containing the *fallback*
1592
- // children -- we skip over the primary children entirely.
1593
- let next;
1587
if (current === null) {
1588
if (enableSuspenseServerRenderer) {
1589
// If we're currently hydrating, try to hydrate this boundary.
1590
// But only if this has a fallback.
1591
if (nextProps.fallback !== undefined) {
1592
tryToClaimNextHydratableInstance(workInProgress);
1600
- // This could've changed the tag if this was a dehydrated suspense component.
1601
- if (workInProgress.tag === DehydratedSuspenseComponent) {
1602
- popSuspenseContext(workInProgress);
1603
- return updateDehydratedSuspenseComponent(
1604
- null,
1605
- workInProgress,
1606
- renderExpirationTime,
1607
- );
1593
+ // This could've been a dehydrated suspense component.
1594
+ const suspenseState: null | SuspenseState =
1595
+ workInProgress.memoizedState;
1596
+ if (suspenseState !== null) {
1597
+ const dehydrated = suspenseState.dehydrated;
1598
+ if (dehydrated !== null) {
1599
+ return mountDehydratedSuspenseComponent(
1600
+ workInProgress,
1601
+ dehydrated,
1602
+ renderExpirationTime,
1603
+ );
1604
+ }
1605
}
1606
}
1607
}
1643
);
1644
fallbackChildFragment.return = workInProgress;
1645
primaryChildFragment.sibling = fallbackChildFragment;
1649
- child = primaryChildFragment;
1646
// Skip the primary children, and continue working on the
1647
// fallback children.
1652
- next = fallbackChildFragment;
1648
+ workInProgress.memoizedState = SUSPENDED_MARKER;
1649
+ workInProgress.child = primaryChildFragment;
1650
+ return fallbackChildFragment;
1651
} else {
1652
// Mount the primary children without an intermediate fragment fiber.
1653
const nextPrimaryChildren = nextProps.children;
1656
- child = next = mountChildFibers(
1654
+ workInProgress.memoizedState = null;
1655
+ return (workInProgress.child = mountChildFibers(
1656
workInProgress,
1657
null,
1658
nextPrimaryChildren,
1659
renderExpirationTime,
1661
- );
1660
+ ));
1661
}
1662
} else {
1663
// This is an update. This branch is more complicated because we need to
1664
// ensure the state of the primary children is preserved.
1666
- const prevState = current.memoizedState;
1667
- const prevDidTimeout = prevState !== null;
1668
- if (prevDidTimeout) {
1665
+ const prevState: null | SuspenseState = current.memoizedState;
1666
+ if (prevState !== null) {
1667
+ if (enableSuspenseServerRenderer) {
1668
+ const dehydrated = prevState.dehydrated;
1669
+ if (dehydrated !== null) {
1670
+ if (!didSuspend) {
1671
+ return updateDehydratedSuspenseComponent(
1672
+ current,
1673
+ workInProgress,
1674
+ dehydrated,
1675
+ renderExpirationTime,
1676
+ );
1677
+ } else if (
1678
+ (workInProgress.memoizedState: null | SuspenseState) !== null
1679
+ ) {
1680
+ // Something suspended and we should still be in dehydrated mode.
1681
+ // Leave the existing child in place.
1682
+ workInProgress.child = current.child;
1683
+ // The dehydrated completion pass expects this flag to be there
1684
+ // but the normal suspense pass doesn't.
1685
+ workInProgress.effectTag |= DidCapture;
1686
+ return null;
1687
+ } else {
1688
+ // Suspended but we should no longer be in dehydrated mode.
1689
+ // Therefore we now have to render the fallback. Wrap the children
1690
+ // in a fragment fiber to keep them separate from the fallback
1691
+ // children.
1692
+ const nextFallbackChildren = nextProps.fallback;
1693
+ const primaryChildFragment = createFiberFromFragment(
1694
+ // It shouldn't matter what the pending props are because we aren't
1695
+ // going to render this fragment.
1696
+ null,
1697
+ mode,
1698
+ NoWork,
1699
+ null,
1700
+ );
1701
+ primaryChildFragment.return = workInProgress;
1702
+
1703
+ // This is always null since we never want the previous child
1704
+ // that we're not going to hydrate.
1705
+ primaryChildFragment.child = null;
1706
+
1707
+ if ((workInProgress.mode & BatchedMode) === NoMode) {
1708
+ // Outside of batched mode, we commit the effects from the
1709
+ // partially completed, timed-out tree, too.
1710
+ let progressedChild = (primaryChildFragment.child =
1711
+ workInProgress.child);
1712
+ while (progressedChild !== null) {
1713
+ progressedChild.return = primaryChildFragment;
1714
+ progressedChild = progressedChild.sibling;
1715
+ }
1716
+ } else {
1717
+ // We will have dropped the effect list which contains the deletion.
1718
+ // We need to reconcile to delete the current child.
1719
+ reconcileChildFibers(
1720
+ workInProgress,
1721
+ current.child,
1722
+ null,
1723
+ renderExpirationTime,
1724
+ );
1725
+ }
1726
+
1727
+ // Because primaryChildFragment is a new fiber that we're inserting as the
1728
+ // parent of a new tree, we need to set its treeBaseDuration.
1729
+ if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1730
+ // treeBaseDuration is the sum of all the child tree base durations.
1731
+ let treeBaseDuration = 0;
1732
+ let hiddenChild = primaryChildFragment.child;
1733
+ while (hiddenChild !== null) {
1734
+ treeBaseDuration += hiddenChild.treeBaseDuration;
1735
+ hiddenChild = hiddenChild.sibling;
1736
+ }
1737
+ primaryChildFragment.treeBaseDuration = treeBaseDuration;
1738
+ }
1739
+
1740
+ // Create a fragment from the fallback children, too.
1741
+ const fallbackChildFragment = createFiberFromFragment(
1742
+ nextFallbackChildren,
1743
+ mode,
1744
+ renderExpirationTime,
1745
+ null,
1746
+ );
1747
+ fallbackChildFragment.return = workInProgress;
1748
+ primaryChildFragment.sibling = fallbackChildFragment;
1749
+ fallbackChildFragment.effectTag |= Placement;
1750
+ primaryChildFragment.childExpirationTime = NoWork;
1751
+
1752
+ workInProgress.memoizedState = SUSPENDED_MARKER;
1753
+ workInProgress.child = primaryChildFragment;
1754
+
1755
+ // Skip the primary children, and continue working on the
1756
+ // fallback children.
1757
+ return fallbackChildFragment;
1758
+ }
1759
+ }
1760
+ }
1761
// The current tree already timed out. That means each child set is
1762
// wrapped in a fragment fiber.
1763
const currentPrimaryChildFragment: Fiber = (current.child: any);
1813
);
1814
fallbackChildFragment.return = workInProgress;
1815
primaryChildFragment.sibling = fallbackChildFragment;
1724
- child = primaryChildFragment;
1816
primaryChildFragment.childExpirationTime = NoWork;
1817
// Skip the primary children, and continue working on the
1818
// fallback children.
1728
- next = fallbackChildFragment;
1819
+ workInProgress.memoizedState = SUSPENDED_MARKER;
1820
+ workInProgress.child = primaryChildFragment;
1821
+ return fallbackChildFragment;
1822
} else {
1823
// No longer suspended. Switch back to showing the primary children,
1824
// and remove the intermediate fragment fiber.
1838
// the stateNode?
1839
1840
// Continue rendering the children, like we normally do.
1748
- child = next = primaryChild;
1841
+ workInProgress.memoizedState = null;
1842
+ return (workInProgress.child = primaryChild);
1843
}
1844
} else {
1845
// The current tree has not already timed out. That means the primary
1907
fallbackChildFragment.return = workInProgress;
1908
primaryChildFragment.sibling = fallbackChildFragment;
1909
fallbackChildFragment.effectTag |= Placement;
1816
- child = primaryChildFragment;
1910
primaryChildFragment.childExpirationTime = NoWork;
1911
// Skip the primary children, and continue working on the
1912
// fallback children.
1820
- next = fallbackChildFragment;
1913
+ workInProgress.memoizedState = SUSPENDED_MARKER;
1914
+ workInProgress.child = primaryChildFragment;
1915
+ return fallbackChildFragment;
1916
} else {
1917
// Still haven't timed out. Continue rendering the children, like we
1918
// normally do.
1919
+ workInProgress.memoizedState = null;
1920
const nextPrimaryChildren = nextProps.children;
1825
- next = child = reconcileChildFibers(
1921
+ return (workInProgress.child = reconcileChildFibers(
1922
workInProgress,
1923
currentPrimaryChild,
1924
nextPrimaryChildren,
1925
renderExpirationTime,
1830
- );
1926
+ ));
1927
}
1928
}
1833
- workInProgress.stateNode = current.stateNode;
1929
}
1835
-
1836
- workInProgress.memoizedState = nextState;
1837
- workInProgress.child = child;
1838
- return next;
1930
}
1931
1932
function retrySuspenseComponentWithoutHydrating(
1934
workInProgress: Fiber,
1935
renderExpirationTime: ExpirationTime,
1936
) {
1846
- // Detach from the current dehydrated boundary.
1847
- current.alternate = null;
1848
- workInProgress.alternate = null;
1849
-
1850
- // Insert a deletion in the effect list.
1851
- let returnFiber = workInProgress.return;
1852
- invariant(
1853
- returnFiber !== null,
1854
- 'Suspense boundaries are never on the root. ' +
1855
- 'This is probably a bug in React.',
1856
- );
1857
- const last = returnFiber.lastEffect;
1858
- if (last !== null) {
1859
- last.nextEffect = current;
1860
- returnFiber.lastEffect = current;
1861
- } else {
1862
- returnFiber.firstEffect = returnFiber.lastEffect = current;
1863
- }
1864
- current.nextEffect = null;
1865
- current.effectTag = Deletion;
1866
-
1867
- popSuspenseContext(workInProgress);
1868
-
1869
- // Upgrade this work in progress to a real Suspense component.
1870
- workInProgress.tag = SuspenseComponent;
1871
- workInProgress.stateNode = null;
1937
+ // We're now not suspended nor dehydrated.
1938
workInProgress.memoizedState = null;
1873
- // This is now an insertion.
1874
- workInProgress.effectTag |= Placement;
1875
- // Retry as a real Suspense component.
1876
- return updateSuspenseComponent(null, workInProgress, renderExpirationTime);
1939
+ // Retry with the full children.
1940
+ const nextProps = workInProgress.pendingProps;
1941
+ const nextChildren = nextProps.children;
1942
+ // This will ensure that the children get Placement effects and
1943
+ // that the old child gets a Deletion effect.
1944
+ // We could also call forceUnmountCurrentAndReconcile.
1945
+ reconcileChildren(
1946
+ current,
1947
+ workInProgress,
1948
+ nextChildren,
1949
+ renderExpirationTime,
1950
+ );
1951
+ return workInProgress.child;
1952
}
1953
1879
-function updateDehydratedSuspenseComponent(
1880
- current: Fiber | null,
1954
+function mountDehydratedSuspenseComponent(
1955
workInProgress: Fiber,
1956
+ suspenseInstance: SuspenseInstance,
1957
renderExpirationTime: ExpirationTime,
1883
-) {
1884
- pushSuspenseContext(
1885
- workInProgress,
1886
- setDefaultShallowSuspenseContext(suspenseStackCursor.current),
1887
- );
1888
- const suspenseInstance = (workInProgress.stateNode: SuspenseInstance);
1889
- if (current === null) {
1890
- // During the first pass, we'll bail out and not drill into the children.
1891
- // Instead, we'll leave the content in place and try to hydrate it later.
1892
- if (isSuspenseInstanceFallback(suspenseInstance)) {
1893
- // This is a client-only boundary. Since we won't get any content from the server
1894
- // for this, we need to schedule that at a higher priority based on when it would
1895
- // have timed out. In theory we could render it in this pass but it would have the
1896
- // wrong priority associated with it and will prevent hydration of parent path.
1897
- // Instead, we'll leave work left on it to render it in a separate commit.
1898
-
1899
- // TODO This time should be the time at which the server rendered response that is
1900
- // a parent to this boundary was displayed. However, since we currently don't have
1901
- // a protocol to transfer that time, we'll just estimate it by using the current
1902
- // time. This will mean that Suspense timeouts are slightly shifted to later than
1903
- // they should be.
1904
- let serverDisplayTime = requestCurrentTime();
1905
- // Schedule a normal pri update to render this content.
1906
- let newExpirationTime = computeAsyncExpiration(serverDisplayTime);
1907
- if (enableSchedulerTracing) {
1908
- markSpawnedWork(newExpirationTime);
1909
- }
1910
- workInProgress.expirationTime = newExpirationTime;
1911
- } else {
1912
- // We'll continue hydrating the rest at offscreen priority since we'll already
1913
- // be showing the right content coming from the server, it is no rush.
1914
- workInProgress.expirationTime = Never;
1915
- if (enableSchedulerTracing) {
1916
- markSpawnedWork(Never);
1917
- }
1958
+): null | Fiber {
1959
+ // During the first pass, we'll bail out and not drill into the children.
1960
+ // Instead, we'll leave the content in place and try to hydrate it later.
1961
+ if ((workInProgress.mode & BatchedMode) === NoMode) {
1962
+ if (__DEV__) {
1963
+ warning(
1964
+ false,
1965
+ 'Cannot hydrate Suspense in legacy mode. Switch from ' +
1966
+ 'ReactDOM.hydrate(element, container) to ' +
1967
+ 'ReactDOM.unstable_createSyncRoot(container, { hydrate: true })' +
1968
+ '.render(element) or remove the Suspense components from ' +
1969
+ 'the server rendered components.',
1970
+ );
1971
+ }
1972
+ workInProgress.expirationTime = Sync;
1973
+ } else if (isSuspenseInstanceFallback(suspenseInstance)) {
1974
+ // This is a client-only boundary. Since we won't get any content from the server
1975
+ // for this, we need to schedule that at a higher priority based on when it would
1976
+ // have timed out. In theory we could render it in this pass but it would have the
1977
+ // wrong priority associated with it and will prevent hydration of parent path.
1978
+ // Instead, we'll leave work left on it to render it in a separate commit.
1979
+
1980
+ // TODO This time should be the time at which the server rendered response that is
1981
+ // a parent to this boundary was displayed. However, since we currently don't have
1982
+ // a protocol to transfer that time, we'll just estimate it by using the current
1983
+ // time. This will mean that Suspense timeouts are slightly shifted to later than
1984
+ // they should be.
1985
+ let serverDisplayTime = requestCurrentTime();
1986
+ // Schedule a normal pri update to render this content.
1987
+ let newExpirationTime = computeAsyncExpiration(serverDisplayTime);
1988
+ if (enableSchedulerTracing) {
1989
+ markSpawnedWork(newExpirationTime);
1990
+ }
1991
+ workInProgress.expirationTime = newExpirationTime;
1992
+ } else {
1993
+ // We'll continue hydrating the rest at offscreen priority since we'll already
1994
+ // be showing the right content coming from the server, it is no rush.
1995
+ workInProgress.expirationTime = Never;
1996
+ if (enableSchedulerTracing) {
1997
+ markSpawnedWork(Never);
1998
}
1919
-
1920
- return null;
1921
- }
1922
-
1923
- if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
1924
- // Something suspended. Leave the existing children in place.
1925
- // TODO: In non-concurrent mode, should we commit the nodes we have hydrated so far?
1926
- workInProgress.child = null;
1927
- return null;
1999
}
2000
+ return null;
2001
+}
2002
2003
+function updateDehydratedSuspenseComponent(
2004
+ current: Fiber,
2005
+ workInProgress: Fiber,
2006
+ suspenseInstance: SuspenseInstance,
2007
+ renderExpirationTime: ExpirationTime,
2008
+): null | Fiber {
2009
// We should never be hydrating at this point because it is the first pass,
2010
// but after we've already committed once.
2011
warnIfHydrating();
2012
2013
+ if ((workInProgress.mode & BatchedMode) === NoMode) {
2014
+ return retrySuspenseComponentWithoutHydrating(
2015
+ current,
2016
+ workInProgress,
2017
+ renderExpirationTime,
2018
+ );
2019
+ }
2020
+
2021
if (isSuspenseInstanceFallback(suspenseInstance)) {
2022
// This boundary is in a permanent fallback state. In this case, we'll never
2023
// get an update and we'll never be able to hydrate the final content. Let's just try the
2054
// these should update this boundary to the permanent Fallback state instead.
2055
// Mark it as having captured (i.e. suspended).
2056
workInProgress.effectTag |= DidCapture;
1970
- // Leave the children in place. I.e. empty.
1971
- workInProgress.child = null;
2057
+ // Leave the child in place. I.e. the dehydrated fragment.
2058
+ workInProgress.child = current.child;
2059
// Register a callback to retry this boundary once the server has sent the result.
2060
registerSuspenseInstanceRetry(
2061
suspenseInstance,
2064
return null;
2065
} else {
2066
// This is the first attempt.
1980
- reenterHydrationStateFromDehydratedSuspenseInstance(workInProgress);
2067
+ reenterHydrationStateFromDehydratedSuspenseInstance(
2068
+ workInProgress,
2069
+ suspenseInstance,
2070
+ );
2071
const nextProps = workInProgress.pendingProps;
2072
const nextChildren = nextProps.children;
2073
workInProgress.child = mountChildFibers(
2822
break;
2823
case SuspenseComponent: {
2824
const state: SuspenseState | null = workInProgress.memoizedState;
2735
- const didTimeout = state !== null;
2736
- if (didTimeout) {
2825
+ if (state !== null) {
2826
+ if (enableSuspenseServerRenderer) {
2827
+ if (state.dehydrated !== null) {
2828
+ pushSuspenseContext(
2829
+ workInProgress,
2830
+ setDefaultShallowSuspenseContext(suspenseStackCursor.current),
2831
+ );
2832
+ // We know that this component will suspend again because if it has
2833
+ // been unsuspended it has committed as a resolved Suspense component.
2834
+ // If it needs to be retried, it should have work scheduled on it.
2835
+ workInProgress.effectTag |= DidCapture;
2836
+ break;
2837
+ }
2838
+ }
2839
+
2840
// If this boundary is currently timed out, we need to decide
2841
// whether to retry the primary children, or to skip over it and
2842
// go straight to the fallback. Check the priority of the primary
2883
}
2884
break;
2885
}
2783
- case DehydratedSuspenseComponent: {
2784
- if (enableSuspenseServerRenderer) {
2785
- pushSuspenseContext(
2786
- workInProgress,
2787
- setDefaultShallowSuspenseContext(suspenseStackCursor.current),
2788
- );
2789
- // We know that this component will suspend again because if it has
2790
- // been unsuspended it has committed as a regular Suspense component.
2791
- // If it needs to be retried, it should have work scheduled on it.
2792
- workInProgress.effectTag |= DidCapture;
2793
- }
2794
- break;
2795
- }
2886
case SuspenseListComponent: {
2887
const didSuspendBefore =
2888
(current.effectTag & DidCapture) !== NoEffect;
3100
renderExpirationTime,
3101
);
3102
}
3013
- case DehydratedSuspenseComponent: {
3014
- if (enableSuspenseServerRenderer) {
3015
- return updateDehydratedSuspenseComponent(
3016
- current,
3017
- workInProgress,
3018
- renderExpirationTime,
3019
- );
3020
- }
3021
- break;
3022
- }
3103
case SuspenseListComponent: {
3104
return updateSuspenseListComponent(
3105
current,