1737
workInProgress,
1738
renderExpirationTime,
1739
) {
1740
- const mode = workInProgress.mode;
1740
const nextProps = workInProgress.pendingProps;
1741
1742
// This is used by DevTools to force a boundary to suspend.
1748
1749
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
1750
1752
- let nextDidTimeout = false;
1751
+ let showFallback = false;
1752
const didSuspend = (workInProgress.effectTag & DidCapture) !== NoEffect;
1753
1754
if (
1762
) {
1763
// Something in this boundary's subtree already suspended. Switch to
1764
// rendering the fallback children.
1766
- nextDidTimeout = true;
1765
+ showFallback = true;
1766
workInProgress.effectTag &= ~DidCapture;
1767
} else {
1768
// Attempting the main content
1791
1792
pushSuspenseContext(workInProgress, suspenseContext);
1793
1795
- // This next part is a bit confusing. If the children timeout, we switch to
1796
- // showing the fallback children in place of the "primary" children.
1797
- // However, we don't want to delete the primary children because then their
1798
- // state will be lost (both the React state and the host state, e.g.
1799
- // uncontrolled form inputs). Instead we keep them mounted and hide them.
1800
- // Both the fallback children AND the primary children are rendered at the
1801
- // same time. Once the primary children are un-suspended, we can delete
1802
- // the fallback children — don't need to preserve their state.
1794
+ // OK, the next part is confusing. We're about to reconcile the Suspense
1795
+ // boundary's children. This involves some custom reconcilation logic. Two
1796
+ // main reasons this is so complicated.
1797
//
1804
- // The two sets of children are siblings in the host environment, but
1805
- // semantically, for purposes of reconciliation, they are two separate sets.
1806
- // So we store them using two fragment fibers.
1798
+ // First, Legacy Mode has different semantics for backwards compatibility. The
1799
+ // primary tree will commit in an inconsistent state, so when we do the
1800
+ // second pass to render the fallback, we do some exceedingly, uh, clever
1801
+ // hacks to make that not totally break. Like transferring effects and
1802
+ // deletions from hidden tree. In Concurrent Mode, it's much simpler,
1803
+ // because we bailout on the primary tree completely and leave it in its old
1804
+ // state, no effects. Same as what we do for Offscreen (except that
1805
+ // Offscreen doesn't have the first render pass).
1806
//
1808
- // However, we want to avoid allocating extra fibers for every placeholder.
1809
- // They're only necessary when the children time out, because that's the
1810
- // only time when both sets are mounted.
1807
+ // Second is hydration. During hydration, the Suspense fiber has a slightly
1808
+ // different layout, where the child points to a dehydrated fragment, which
1809
+ // contains the DOM rendered by the server.
1810
//
1812
- // So, the extra fragment fibers are only used if the children time out.
1813
- // Otherwise, we render the primary children directly. This requires some
1814
- // custom reconciliation logic to preserve the state of the primary
1815
- // children. It's essentially a very basic form of re-parenting.
1816
-
1811
+ // Third, even if you set all that aside, Suspense is like error boundaries in
1812
+ // that we first we try to render one tree, and if that fails, we render again
1813
+ // and switch to a different tree. Like a try/catch block. So we have to track
1814
+ // which branch we're currently rendering. Ideally we would model this using
1815
+ // a stack.
1816
if (current === null) {
1817
+ // Initial mount
1818
// If we're currently hydrating, try to hydrate this boundary.
1819
// But only if this has a fallback.
1820
if (nextProps.fallback !== undefined) {
1836
}
1837
}
1838
1839
- // This is the initial mount. This branch is pretty simple because there's
1840
- // no previous state that needs to be preserved.
1841
- if (nextDidTimeout) {
1842
- // Mount separate fragments for primary and fallback children.
1839
+ if (showFallback) {
1840
const nextFallbackChildren = nextProps.fallback;
1844
- const primaryChildFragment = createFiberFromFragment(
1845
- null,
1846
- mode,
1847
- NoWork,
1848
- null,
1849
- );
1850
- primaryChildFragment.return = workInProgress;
1851
-
1852
- if ((workInProgress.mode & BlockingMode) === NoMode) {
1853
- // Outside of blocking mode, we commit the effects from the
1854
- // partially completed, timed-out tree, too.
1855
- const progressedState: SuspenseState = workInProgress.memoizedState;
1856
- const progressedPrimaryChild: Fiber | null =
1857
- progressedState !== null
1858
- ? (workInProgress.child: any).child
1859
- : (workInProgress.child: any);
1860
- primaryChildFragment.child = progressedPrimaryChild;
1861
- let progressedChild = progressedPrimaryChild;
1862
- while (progressedChild !== null) {
1863
- progressedChild.return = primaryChildFragment;
1864
- progressedChild = progressedChild.sibling;
1865
- }
1866
- }
1867
-
1868
- const fallbackChildFragment = createFiberFromFragment(
1841
+ const fallbackFragment = mountSuspenseFallbackChildren(
1842
+ workInProgress,
1843
nextFallbackChildren,
1870
- mode,
1844
renderExpirationTime,
1872
- null,
1845
);
1874
- fallbackChildFragment.return = workInProgress;
1875
- primaryChildFragment.sibling = fallbackChildFragment;
1876
- // Skip the primary children, and continue working on the
1877
- // fallback children.
1846
workInProgress.memoizedState = mountSuspenseState(renderExpirationTime);
1879
- workInProgress.child = primaryChildFragment;
1880
- return fallbackChildFragment;
1847
+ return fallbackFragment;
1848
} else {
1882
- // Mount the primary children without an intermediate fragment fiber.
1849
const nextPrimaryChildren = nextProps.children;
1884
- workInProgress.memoizedState = null;
1885
- return (workInProgress.child = mountChildFibers(
1850
+ return mountSuspensePrimaryChildren(
1851
workInProgress,
1887
- null,
1852
nextPrimaryChildren,
1853
renderExpirationTime,
1890
- ));
1854
+ );
1855
}
1856
} else {
1893
- // This is an update. This branch is more complicated because we need to
1894
- // ensure the state of the primary children is preserved.
1857
+ // This is an update.
1858
+
1859
+ // If the current fiber has a SuspenseState, that means it's already showing
1860
+ // a fallback.
1861
const prevState: null | SuspenseState = current.memoizedState;
1862
if (prevState !== null) {
1863
+ // The current tree is already showing a fallback
1864
+
1865
+ // Special path for hydration
1866
if (enableSuspenseServerRenderer) {
1867
const dehydrated = prevState.dehydrated;
1868
if (dehydrated !== null) {
1886
return null;
1887
} else {
1888
// Suspended but we should no longer be in dehydrated mode.
1920
- // Therefore we now have to render the fallback. Wrap the children
1921
- // in a fragment fiber to keep them separate from the fallback
1922
- // children.
1889
+ // Therefore we now have to render the fallback.
1890
const nextFallbackChildren = nextProps.fallback;
1924
- const primaryChildFragment = createFiberFromFragment(
1925
- // It shouldn't matter what the pending props are because we aren't
1926
- // going to render this fragment.
1927
- null,
1928
- mode,
1929
- (NoWork: ExpirationTimeOpaque),
1930
- null,
1931
- );
1932
- primaryChildFragment.return = workInProgress;
1933
-
1934
- // This is always null since we never want the previous child
1935
- // that we're not going to hydrate.
1936
- primaryChildFragment.child = null;
1937
-
1938
- if ((workInProgress.mode & BlockingMode) === NoMode) {
1939
- // Outside of blocking mode, we commit the effects from the
1940
- // partially completed, timed-out tree, too.
1941
- let progressedChild = (primaryChildFragment.child =
1942
- workInProgress.child);
1943
- while (progressedChild !== null) {
1944
- progressedChild.return = primaryChildFragment;
1945
- progressedChild = progressedChild.sibling;
1946
- }
1947
- } else {
1948
- // We will have dropped the effect list which contains the deletion.
1949
- // We need to reconcile to delete the current child.
1950
- reconcileChildFibers(
1951
- workInProgress,
1952
- current.child,
1953
- null,
1954
- renderExpirationTime,
1955
- );
1956
- }
1957
-
1958
- // Because primaryChildFragment is a new fiber that we're inserting as the
1959
- // parent of a new tree, we need to set its treeBaseDuration.
1960
- if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1961
- // treeBaseDuration is the sum of all the child tree base durations.
1962
- let treeBaseDuration = 0;
1963
- let hiddenChild = primaryChildFragment.child;
1964
- while (hiddenChild !== null) {
1965
- treeBaseDuration += hiddenChild.treeBaseDuration;
1966
- hiddenChild = hiddenChild.sibling;
1967
- }
1968
- primaryChildFragment.treeBaseDuration = treeBaseDuration;
1969
- }
1970
-
1971
- // Create a fragment from the fallback children, too.
1972
- const fallbackChildFragment = createFiberFromFragment(
1973
- nextFallbackChildren,
1974
- mode,
1975
- renderExpirationTime,
1976
- null,
1977
- );
1978
- fallbackChildFragment.return = workInProgress;
1979
- primaryChildFragment.sibling = fallbackChildFragment;
1980
- fallbackChildFragment.effectTag |= Placement;
1981
- primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree(
1891
+ const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
1892
current,
1893
workInProgress,
1894
+ nextFallbackChildren,
1895
renderExpirationTime,
1896
);
1897
+
1898
workInProgress.memoizedState = updateSuspenseState(
1899
current.memoizedState,
1900
renderExpirationTime,
1901
);
1990
- workInProgress.child = primaryChildFragment;
1902
1992
- // Skip the primary children, and continue working on the
1993
- // fallback children.
1903
return fallbackChildFragment;
1904
}
1905
}
1906
}
1998
- // The current tree already timed out. That means each child set is
1999
- // wrapped in a fragment fiber.
2000
- const currentPrimaryChildFragment: Fiber = (current.child: any);
2001
- const currentFallbackChildFragment: Fiber = (currentPrimaryChildFragment.sibling: any);
2002
- if (nextDidTimeout) {
2003
- // Still timed out. Reuse the current primary children by cloning
2004
- // its fragment. We're going to skip over these entirely.
2005
- const nextFallbackChildren = nextProps.fallback;
2006
- const primaryChildFragment = createWorkInProgress(
2007
- currentPrimaryChildFragment,
2008
- currentPrimaryChildFragment.pendingProps,
2009
- );
2010
- primaryChildFragment.return = workInProgress;
2011
-
2012
- if ((workInProgress.mode & BlockingMode) === NoMode) {
2013
- // Outside of blocking mode, we commit the effects from the
2014
- // partially completed, timed-out tree, too.
2015
- const progressedState: SuspenseState = workInProgress.memoizedState;
2016
- const progressedPrimaryChild: Fiber | null =
2017
- progressedState !== null
2018
- ? (workInProgress.child: any).child
2019
- : (workInProgress.child: any);
2020
- if (progressedPrimaryChild !== currentPrimaryChildFragment.child) {
2021
- primaryChildFragment.child = progressedPrimaryChild;
2022
- let progressedChild = progressedPrimaryChild;
2023
- while (progressedChild !== null) {
2024
- progressedChild.return = primaryChildFragment;
2025
- progressedChild = progressedChild.sibling;
2026
- }
2027
- }
2028
- }
2029
-
2030
- // Because primaryChildFragment is a new fiber that we're inserting as the
2031
- // parent of a new tree, we need to set its treeBaseDuration.
2032
- if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
2033
- // treeBaseDuration is the sum of all the child tree base durations.
2034
- let treeBaseDuration = 0;
2035
- let hiddenChild = primaryChildFragment.child;
2036
- while (hiddenChild !== null) {
2037
- treeBaseDuration += hiddenChild.treeBaseDuration;
2038
- hiddenChild = hiddenChild.sibling;
2039
- }
2040
- primaryChildFragment.treeBaseDuration = treeBaseDuration;
2041
- }
1907
2043
- // Clone the fallback child fragment, too. These we'll continue
2044
- // working on.
2045
- const fallbackChildFragment = createWorkInProgress(
2046
- currentFallbackChildFragment,
1908
+ if (showFallback) {
1909
+ const nextFallbackChildren = nextProps.fallback;
1910
+ const fallbackChildFragment = updateSuspenseFallbackChildren(
1911
+ current,
1912
+ workInProgress,
1913
nextFallbackChildren,
1914
+ renderExpirationTime,
1915
);
2049
- fallbackChildFragment.return = workInProgress;
2050
- primaryChildFragment.sibling = fallbackChildFragment;
1916
+ const primaryChildFragment: Fiber = (workInProgress.child: any);
1917
primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree(
1918
current,
1919
workInProgress,
1920
renderExpirationTime,
1921
);
2056
- // Skip the primary children, and continue working on the
2057
- // fallback children.
1922
workInProgress.memoizedState = updateSuspenseState(
1923
current.memoizedState,
1924
renderExpirationTime,
1925
);
2062
- workInProgress.child = primaryChildFragment;
1926
return fallbackChildFragment;
1927
} else {
2065
- // No longer suspended. Switch back to showing the primary children,
2066
- // and remove the intermediate fragment fiber.
1928
const nextPrimaryChildren = nextProps.children;
2068
- const currentPrimaryChild = currentPrimaryChildFragment.child;
2069
- const primaryChild = reconcileChildFibers(
1929
+ const primaryChildFragment = updateSuspensePrimaryChildren(
1930
+ current,
1931
workInProgress,
2071
- currentPrimaryChild,
1932
nextPrimaryChildren,
1933
renderExpirationTime,
1934
);
2075
-
2076
- // If this render doesn't suspend, we need to delete the fallback
2077
- // children. Wait until the complete phase, after we've confirmed the
2078
- // fallback is no longer needed.
2079
- // TODO: Would it be better to store the fallback fragment on
2080
- // the stateNode?
2081
-
2082
- // Continue rendering the children, like we normally do.
1935
workInProgress.memoizedState = null;
2084
- return (workInProgress.child = primaryChild);
1936
+ return primaryChildFragment;
1937
}
1938
} else {
2087
- // The current tree has not already timed out. That means the primary
2088
- // children are not wrapped in a fragment fiber.
2089
- const currentPrimaryChild = current.child;
2090
- if (nextDidTimeout) {
2091
- // Timed out. Wrap the children in a fragment fiber to keep them
2092
- // separate from the fallback children.
1939
+ // The current tree is not already showing a fallback.
1940
+ if (showFallback) {
1941
+ // Timed out.
1942
const nextFallbackChildren = nextProps.fallback;
2094
- const primaryChildFragment = createFiberFromFragment(
2095
- // It shouldn't matter what the pending props are because we aren't
2096
- // going to render this fragment.
2097
- null,
2098
- mode,
2099
- NoWork,
2100
- null,
2101
- );
2102
- primaryChildFragment.return = workInProgress;
2103
- primaryChildFragment.child = currentPrimaryChild;
2104
- if (currentPrimaryChild !== null) {
2105
- currentPrimaryChild.return = primaryChildFragment;
2106
- }
2107
-
2108
- // Even though we're creating a new fiber, there are no new children,
2109
- // because we're reusing an already mounted tree. So we don't need to
2110
- // schedule a placement.
2111
- // primaryChildFragment.effectTag |= Placement;
2112
-
2113
- if ((workInProgress.mode & BlockingMode) === NoMode) {
2114
- // Outside of blocking mode, we commit the effects from the
2115
- // partially completed, timed-out tree, too.
2116
- const progressedState: SuspenseState = workInProgress.memoizedState;
2117
- const progressedPrimaryChild: Fiber | null =
2118
- progressedState !== null
2119
- ? (workInProgress.child: any).child
2120
- : (workInProgress.child: any);
2121
- primaryChildFragment.child = progressedPrimaryChild;
2122
- let progressedChild = progressedPrimaryChild;
2123
- while (progressedChild !== null) {
2124
- progressedChild.return = primaryChildFragment;
2125
- progressedChild = progressedChild.sibling;
2126
- }
2127
- }
2128
-
2129
- // Because primaryChildFragment is a new fiber that we're inserting as the
2130
- // parent of a new tree, we need to set its treeBaseDuration.
2131
- if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
2132
- // treeBaseDuration is the sum of all the child tree base durations.
2133
- let treeBaseDuration = 0;
2134
- let hiddenChild = primaryChildFragment.child;
2135
- while (hiddenChild !== null) {
2136
- treeBaseDuration += hiddenChild.treeBaseDuration;
2137
- hiddenChild = hiddenChild.sibling;
2138
- }
2139
- primaryChildFragment.treeBaseDuration = treeBaseDuration;
2140
- }
2141
-
2142
- // Create a fragment from the fallback children, too.
2143
- const fallbackChildFragment = createFiberFromFragment(
1943
+ const fallbackChildFragment = updateSuspenseFallbackChildren(
1944
+ current,
1945
+ workInProgress,
1946
nextFallbackChildren,
2145
- mode,
1947
renderExpirationTime,
2147
- null,
1948
);
2149
- fallbackChildFragment.return = workInProgress;
2150
- primaryChildFragment.sibling = fallbackChildFragment;
2151
- fallbackChildFragment.effectTag |= Placement;
1949
+ const primaryChildFragment: Fiber = (workInProgress.child: any);
1950
primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree(
1951
current,
1952
workInProgress,
1955
// Skip the primary children, and continue working on the
1956
// fallback children.
1957
workInProgress.memoizedState = mountSuspenseState(renderExpirationTime);
2160
- workInProgress.child = primaryChildFragment;
1958
return fallbackChildFragment;
1959
} else {
1960
// Still haven't timed out. Continue rendering the children, like we
1961
// normally do.
2165
- workInProgress.memoizedState = null;
1962
const nextPrimaryChildren = nextProps.children;
2167
- return (workInProgress.child = reconcileChildFibers(
1963
+ const primaryChildFragment = updateSuspensePrimaryChildren(
1964
+ current,
1965
workInProgress,
2169
- currentPrimaryChild,
1966
nextPrimaryChildren,
1967
renderExpirationTime,
2172
- ));
1968
+ );
1969
+ workInProgress.memoizedState = null;
1970
+ return primaryChildFragment;
1971
}
1972
}
1973
}
1974
}
1975
1976
+function mountSuspensePrimaryChildren(
1977
+ workInProgress,
1978
+ primaryChildren,
1979
+ renderExpirationTime,
1980
+) {
1981
+ const mode = workInProgress.mode;
1982
+ const primaryChildFragment = createFiberFromFragment(
1983
+ primaryChildren,
1984
+ mode,
1985
+ renderExpirationTime,
1986
+ null,
1987
+ );
1988
+ primaryChildFragment.return = workInProgress;
1989
+ workInProgress.child = primaryChildFragment;
1990
+ return primaryChildFragment;
1991
+}
1992
+
1993
+function mountSuspenseFallbackChildren(
1994
+ workInProgress,
1995
+ fallbackChildren,
1996
+ renderExpirationTime,
1997
+) {
1998
+ const mode = workInProgress.mode;
1999
+
2000
+ const progressedPrimaryFragment: Fiber | null = workInProgress.child;
2001
+
2002
+ let primaryChildFragment;
2003
+ let fallbackChildFragment;
2004
+ if ((mode & BlockingMode) === NoMode && progressedPrimaryFragment !== null) {
2005
+ // In legacy mode, we commit the primary tree as if it successfully
2006
+ // completed, even though it's in an inconsistent state.
2007
+ primaryChildFragment = progressedPrimaryFragment;
2008
+ primaryChildFragment.childExpirationTime_opaque = NoWork;
2009
+
2010
+ if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
2011
+ // Reset the durations from the first pass so they aren't included in the
2012
+ // final amounts. This seems counterintuitive, since we're intentionally
2013
+ // not measuring part of the render phase, but this makes it match what we
2014
+ // do in Concurrent Mode.
2015
+ primaryChildFragment.actualDuration = 0;
2016
+ primaryChildFragment.actualStartTime = -1;
2017
+ primaryChildFragment.selfBaseDuration = 0;
2018
+ primaryChildFragment.treeBaseDuration = 0;
2019
+ }
2020
+
2021
+ fallbackChildFragment = createFiberFromFragment(
2022
+ fallbackChildren,
2023
+ mode,
2024
+ renderExpirationTime,
2025
+ null,
2026
+ );
2027
+ } else {
2028
+ primaryChildFragment = createFiberFromFragment(null, mode, NoWork, null);
2029
+ fallbackChildFragment = createFiberFromFragment(
2030
+ fallbackChildren,
2031
+ mode,
2032
+ renderExpirationTime,
2033
+ null,
2034
+ );
2035
+ }
2036
+
2037
+ primaryChildFragment.return = workInProgress;
2038
+ fallbackChildFragment.return = workInProgress;
2039
+ primaryChildFragment.sibling = fallbackChildFragment;
2040
+ workInProgress.child = primaryChildFragment;
2041
+ return fallbackChildFragment;
2042
+}
2043
+
2044
+function updateSuspensePrimaryChildren(
2045
+ current,
2046
+ workInProgress,
2047
+ primaryChildren,
2048
+ renderExpirationTime,
2049
+) {
2050
+ const currentPrimaryChildFragment: Fiber = (current.child: any);
2051
+ const currentFallbackChildFragment: Fiber | null =
2052
+ currentPrimaryChildFragment.sibling;
2053
+
2054
+ const primaryChildFragment = createWorkInProgress(
2055
+ currentPrimaryChildFragment,
2056
+ primaryChildren,
2057
+ );
2058
+ if ((workInProgress.mode & BlockingMode) === NoMode) {
2059
+ primaryChildFragment.expirationTime_opaque = renderExpirationTime;
2060
+ }
2061
+ primaryChildFragment.return = workInProgress;
2062
+ primaryChildFragment.sibling = null;
2063
+ if (currentFallbackChildFragment !== null) {
2064
+ // Delete the fallback child fragment
2065
+ currentFallbackChildFragment.nextEffect = null;
2066
+ currentFallbackChildFragment.effectTag = Deletion;
2067
+ workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment;
2068
+ }
2069
+
2070
+ workInProgress.child = primaryChildFragment;
2071
+ return primaryChildFragment;
2072
+}
2073
+
2074
+function updateSuspenseFallbackChildren(
2075
+ current,
2076
+ workInProgress,
2077
+ fallbackChildren,
2078
+ renderExpirationTime,
2079
+) {
2080
+ const mode = workInProgress.mode;
2081
+ const currentPrimaryChildFragment: Fiber = (current.child: any);
2082
+ const currentFallbackChildFragment: Fiber | null =
2083
+ currentPrimaryChildFragment.sibling;
2084
+
2085
+ let primaryChildFragment;
2086
+ if ((mode & BlockingMode) === NoMode) {
2087
+ // In legacy mode, we commit the primary tree as if it successfully
2088
+ // completed, even though it's in an inconsistent state.
2089
+ const progressedPrimaryFragment: Fiber = (workInProgress.child: any);
2090
+ primaryChildFragment = progressedPrimaryFragment;
2091
+ primaryChildFragment.childExpirationTime_opaque = NoWork;
2092
+
2093
+ if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
2094
+ // Reset the durations from the first pass so they aren't included in the
2095
+ // final amounts. This seems counterintuitive, since we're intentionally
2096
+ // not measuring part of the render phase, but this makes it match what we
2097
+ // do in Concurrent Mode.
2098
+ primaryChildFragment.actualDuration = 0;
2099
+ primaryChildFragment.actualStartTime = -1;
2100
+ primaryChildFragment.selfBaseDuration =
2101
+ currentPrimaryChildFragment.selfBaseDuration;
2102
+ primaryChildFragment.treeBaseDuration =
2103
+ currentPrimaryChildFragment.treeBaseDuration;
2104
+ }
2105
+
2106
+ // The fallback fiber was added as a deletion effect during the first pass.
2107
+ // However, since we're going to remain on the fallback, we no longer want
2108
+ // to delete it. So we need to remove it from the list. Deletions are stored
2109
+ // on the same list as effects. We want to keep the effects from the primary
2110
+ // tree. So we copy the primary child fragment's effect list, which does not
2111
+ // include the fallback deletion effect.
2112
+ const progressedLastEffect = primaryChildFragment.lastEffect;
2113
+ if (progressedLastEffect !== null) {
2114
+ workInProgress.firstEffect = primaryChildFragment.firstEffect;
2115
+ workInProgress.lastEffect = progressedLastEffect;
2116
+ progressedLastEffect.nextEffect = null;
2117
+ } else {
2118
+ // TODO: Reset this somewhere else? Lol legacy mode is so weird.
2119
+ workInProgress.firstEffect = workInProgress.lastEffect = null;
2120
+ }
2121
+ } else {
2122
+ primaryChildFragment = createWorkInProgress(
2123
+ currentPrimaryChildFragment,
2124
+ currentPrimaryChildFragment.pendingProps,
2125
+ );
2126
+ }
2127
+ let fallbackChildFragment;
2128
+ if (currentFallbackChildFragment !== null) {
2129
+ fallbackChildFragment = createWorkInProgress(
2130
+ currentFallbackChildFragment,
2131
+ fallbackChildren,
2132
+ );
2133
+ } else {
2134
+ fallbackChildFragment = createFiberFromFragment(
2135
+ fallbackChildren,
2136
+ mode,
2137
+ renderExpirationTime,
2138
+ null,
2139
+ );
2140
+ // Needs a placement effect because the parent (the Suspense boundary) already
2141
+ // mounted but this is a new fiber.
2142
+ fallbackChildFragment.effectTag |= Placement;
2143
+ }
2144
+
2145
+ fallbackChildFragment.return = workInProgress;
2146
+ primaryChildFragment.return = workInProgress;
2147
+ primaryChildFragment.sibling = fallbackChildFragment;
2148
+ workInProgress.child = primaryChildFragment;
2149
+
2150
+ return fallbackChildFragment;
2151
+}
2152
+
2153
function retrySuspenseComponentWithoutHydrating(
2154
current: Fiber,
2155
workInProgress: Fiber,
2156
renderExpirationTime: ExpirationTimeOpaque,
2157
) {
2158
+ // This will add the old fiber to the deletion list
2159
+ reconcileChildFibers(
2160
+ workInProgress,
2161
+ current.child,
2162
+ null,
2163
+ renderExpirationTime,
2164
+ );
2165
+
2166
// We're now not suspended nor dehydrated.
2184
- workInProgress.memoizedState = null;
2185
- // Retry with the full children.
2167
const nextProps = workInProgress.pendingProps;
2187
- const nextChildren = nextProps.children;
2188
- // This will ensure that the children get Placement effects and
2189
- // that the old child gets a Deletion effect.
2190
- // We could also call forceUnmountCurrentAndReconcile.
2191
- reconcileChildren(
2192
- current,
2168
+ const primaryChildren = nextProps.children;
2169
+ const primaryChildFragment = mountSuspensePrimaryChildren(
2170
workInProgress,
2194
- nextChildren,
2171
+ primaryChildren,
2172
renderExpirationTime,
2173
);
2197
- return workInProgress.child;
2174
+ // Needs a placement effect because the parent (the Suspense boundary) already
2175
+ // mounted but this is a new fiber.
2176
+ primaryChildFragment.effectTag |= Placement;
2177
+ workInProgress.memoizedState = null;
2178
+
2179
+ return primaryChildFragment;
2180
+}
2181
+
2182
+function mountSuspenseFallbackAfterRetryWithoutHydrating(
2183
+ current,
2184
+ workInProgress,
2185
+ fallbackChildren,
2186
+ renderExpirationTime,
2187
+) {
2188
+ const mode = workInProgress.mode;
2189
+ const primaryChildFragment = createFiberFromFragment(
2190
+ null,
2191
+ mode,
2192
+ NoWork,
2193
+ null,
2194
+ );
2195
+ const fallbackChildFragment = createFiberFromFragment(
2196
+ fallbackChildren,
2197
+ mode,
2198
+ renderExpirationTime,
2199
+ null,
2200
+ );
2201
+ // Needs a placement effect because the parent (the Suspense
2202
+ // boundary) already mounted but this is a new fiber.
2203
+ fallbackChildFragment.effectTag |= Placement;
2204
+
2205
+ primaryChildFragment.return = workInProgress;
2206
+ fallbackChildFragment.return = workInProgress;
2207
+ primaryChildFragment.sibling = fallbackChildFragment;
2208
+ workInProgress.child = primaryChildFragment;
2209
+
2210
+ if ((workInProgress.mode & BlockingMode) !== NoMode) {
2211
+ // We will have dropped the effect list which contains the
2212
+ // deletion. We need to reconcile to delete the current child.
2213
+ reconcileChildFibers(
2214
+ workInProgress,
2215
+ current.child,
2216
+ null,
2217
+ renderExpirationTime,
2218
+ );
2219
+ }
2220
+
2221
+ return fallbackChildFragment;
2222
}
2223
2224
function mountDehydratedSuspenseComponent(
2370
suspenseInstance,
2371
);
2372
const nextProps = workInProgress.pendingProps;
2349
- const nextChildren = nextProps.children;
2350
- const child = mountChildFibers(
2373
+ const primaryChildren = nextProps.children;
2374
+ const primaryChildFragment = mountSuspensePrimaryChildren(
2375
workInProgress,
2352
- null,
2353
- nextChildren,
2376
+ primaryChildren,
2377
renderExpirationTime,
2378
);
2356
- let node = child;
2357
- while (node) {
2358
- // Mark each child as hydrating. This is a fast path to know whether this
2359
- // tree is part of a hydrating tree. This is used to determine if a child
2360
- // node has fully mounted yet, and for scheduling event replaying.
2361
- // Conceptually this is similar to Placement in that a new subtree is
2362
- // inserted into the React tree here. It just happens to not need DOM
2363
- // mutations because it already exists.
2364
- node.effectTag |= Hydrating;
2365
- node = node.sibling;
2366
- }
2367
- workInProgress.child = child;
2368
- return workInProgress.child;
2379
+ // Mark the children as hydrating. This is a fast path to know whether this
2380
+ // tree is part of a hydrating tree. This is used to determine if a child
2381
+ // node has fully mounted yet, and for scheduling event replaying.
2382
+ // Conceptually this is similar to Placement in that a new subtree is
2383
+ // inserted into the React tree here. It just happens to not need DOM
2384
+ // mutations because it already exists.
2385
+ primaryChildFragment.effectTag |= Hydrating;
2386
+ return primaryChildFragment;
2387
}
2388
}
2389
2705
pushSuspenseContext(workInProgress, suspenseContext);
2706
2707
if ((workInProgress.mode & BlockingMode) === NoMode) {
2690
- // Outside of blocking mode, SuspenseList doesn't work so we just
2708
+ // In legacy mode, SuspenseList doesn't work so we just
2709
// use make it a noop by treating it as the default revealOrder.
2710
workInProgress.memoizedState = null;
2711
} else {
3218
);
3219
} else {
3220
// The primary child fragment does not have pending work marked
3203
- // on it...
3204
-
3205
- // ...usually. There's an unfortunate edge case where the fragment
3206
- // fiber is not part of the return path of the children, so when
3207
- // an update happens, the fragment doesn't get marked during
3208
- // setState. This is something we should consider addressing when
3209
- // we refactor the Fiber data structure. (There's a test with more
3210
- // details; to find it, comment out the following block and see
3211
- // which one fails.)
3212
- //
3213
- // As a workaround, we need to recompute the `childExpirationTime`
3214
- // by bubbling it up from the next level of children. This is
3215
- // based on similar logic in `resetChildExpirationTime`.
3216
- let primaryChild = primaryChildFragment.child;
3217
- while (primaryChild !== null) {
3218
- const childUpdateExpirationTime =
3219
- primaryChild.expirationTime_opaque;
3220
- const childChildExpirationTime =
3221
- primaryChild.childExpirationTime_opaque;
3222
- if (
3223
- isSameOrHigherPriority(
3224
- childUpdateExpirationTime,
3225
- renderExpirationTime,
3226
- ) ||
3227
- isSameOrHigherPriority(
3228
- childChildExpirationTime,
3229
- renderExpirationTime,
3230
- )
3231
- ) {
3232
- // Found a child with an update with sufficient priority.
3233
- // Use the normal path to render the primary children again.
3234
- return updateSuspenseComponent(
3235
- current,
3236
- workInProgress,
3237
- renderExpirationTime,
3238
- );
3239
- }
3240
- primaryChild = primaryChild.sibling;
3241
- }
3242
-
3221
+ // on it
3222
pushSuspenseContext(
3223
workInProgress,
3224
setDefaultShallowSuspenseContext(suspenseStackCursor.current),