@samitouri / QOS-React-2 / commits / 04ccc01d96

Hydration errors should force a client render (#22416)

* Refactor throwException control flow I'm about to add more branches to the Suspense-related logic in `throwException`, so before I do, I split some of the steps into separate functions so that later I can use them in multiple places. This commit does not change any program behavior, only the control flow surrounding existing code. * Hydration errors should force a client render If something errors during hydration, we should try rendering again without hydrating. We'll find the nearest Suspense boundary and force it to client render, discarding the server-rendered content.

Andrew Clark committed Sep 24, 2021 at 13:47 UTC 04ccc01d967091f441f08177a5078cc6034c2a9e
6 files changed +906 -470
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+259
@@ -1783,4 +1783,263 @@ describe('ReactDOMFizzServer', () => {
1783 expect(getVisibleChildren(container)).toEqual(<div>client</div>);
1784 expect(ref.current).toEqual(serverRenderedDiv);
1785 });
1786 +
1787 + // @gate supportsNativeUseSyncExternalStore
1788 + // @gate experimental
1789 + it(
1790 + 'errors during hydration force a client render at the nearest Suspense ' +
1791 + 'boundary, and during the client render it recovers',
1792 + async () => {
1793 + let isClient = false;
1794 +
1795 + function subscribe() {
1796 + return () => {};
1797 + }
1798 + function getClientSnapshot() {
1799 + return 'Yay!';
1800 + }
1801 +
1802 + // At the time of writing, the only API that exposes whether it's currently
1803 + // hydrating is the `getServerSnapshot` API, so I'm using that here to
1804 + // simulate an error during hydration.
1805 + function getServerSnapshot() {
1806 + if (isClient) {
1807 + throw new Error('Hydration error');
1808 + }
1809 + return 'Yay!';
1810 + }
1811 +
1812 + function Child() {
1813 + const value = useSyncExternalStore(
1814 + subscribe,
1815 + getClientSnapshot,
1816 + getServerSnapshot,
1817 + );
1818 + Scheduler.unstable_yieldValue(value);
1819 + return value;
1820 + }
1821 +
1822 + const span1Ref = React.createRef();
1823 + const span2Ref = React.createRef();
1824 + const span3Ref = React.createRef();
1825 +
1826 + function App() {
1827 + return (
1828 + <div>
1829 + <span ref={span1Ref} />
1830 + <Suspense fallback="Loading...">
1831 + <span ref={span2Ref}>
1832 + <Child />
1833 + </span>
1834 + </Suspense>
1835 + <span ref={span3Ref} />
1836 + </div>
1837 + );
1838 + }
1839 +
1840 + await act(async () => {
1841 + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
1842 + <App />,
1843 + writable,
1844 + );
1845 + startWriting();
1846 + });
1847 + expect(Scheduler).toHaveYielded(['Yay!']);
1848 +
1849 + const [span1, span2, span3] = container.getElementsByTagName('span');
1850 +
1851 + // Hydrate the tree. Child will throw during hydration, but not when it
1852 + // falls back to client rendering.
1853 + isClient = true;
1854 + ReactDOM.hydrateRoot(container, <App />);
1855 +
1856 + expect(Scheduler).toFlushAndYield(['Yay!']);
1857 + expect(getVisibleChildren(container)).toEqual(
1858 + <div>
1859 + <span />
1860 + <span>Yay!</span>
1861 + <span />
1862 + </div>,
1863 + );
1864 +
1865 + // The node that's inside the boundary that errored during hydration was
1866 + // not hydrated.
1867 + expect(span2Ref.current).not.toBe(span2);
1868 +
1869 + // But the nodes outside the boundary were.
1870 + expect(span1Ref.current).toBe(span1);
1871 + expect(span3Ref.current).toBe(span3);
1872 + },
1873 + );
1874 +
1875 + // @gate experimental
1876 + it(
1877 + 'errors during hydration force a client render at the nearest Suspense ' +
1878 + 'boundary, and during the client render it fails again',
1879 + async () => {
1880 + // Similar to previous test, but the client render errors, too. We should
1881 + // be able to capture it with an error boundary.
1882 +
1883 + let isClient = false;
1884 +
1885 + class ErrorBoundary extends React.Component {
1886 + state = {error: null};
1887 + static getDerivedStateFromError(error) {
1888 + return {error};
1889 + }
1890 + render() {
1891 + if (this.state.error !== null) {
1892 + return this.state.error.message;
1893 + }
1894 + return this.props.children;
1895 + }
1896 + }
1897 +
1898 + function Child() {
1899 + if (isClient) {
1900 + throw new Error('Oops!');
1901 + }
1902 + Scheduler.unstable_yieldValue('Yay!');
1903 + return 'Yay!';
1904 + }
1905 +
1906 + const span1Ref = React.createRef();
1907 + const span2Ref = React.createRef();
1908 + const span3Ref = React.createRef();
1909 +
1910 + function App() {
1911 + return (
1912 + <ErrorBoundary>
1913 + <span ref={span1Ref} />
1914 + <Suspense fallback="Loading...">
1915 + <span ref={span2Ref}>
1916 + <Child />
1917 + </span>
1918 + </Suspense>
1919 + <span ref={span3Ref} />
1920 + </ErrorBoundary>
1921 + );
1922 + }
1923 +
1924 + await act(async () => {
1925 + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
1926 + <App />,
1927 + writable,
1928 + );
1929 + startWriting();
1930 + });
1931 + expect(Scheduler).toHaveYielded(['Yay!']);
1932 +
1933 + // Hydrate the tree. Child will throw during render.
1934 + isClient = true;
1935 + ReactDOM.hydrateRoot(container, <App />);
1936 +
1937 + expect(Scheduler).toFlushAndYield([]);
1938 + expect(getVisibleChildren(container)).toEqual('Oops!');
1939 + },
1940 + );
1941 +
1942 + // @gate supportsNativeUseSyncExternalStore
1943 + // @gate experimental
1944 + it(
1945 + 'errors during hydration force a client render at the nearest Suspense ' +
1946 + 'boundary, and during the client render it recovers, then a deeper ' +
1947 + 'child suspends',
1948 + async () => {
1949 + let isClient = false;
1950 +
1951 + function subscribe() {
1952 + return () => {};
1953 + }
1954 + function getClientSnapshot() {
1955 + return 'Yay!';
1956 + }
1957 +
1958 + // At the time of writing, the only API that exposes whether it's currently
1959 + // hydrating is the `getServerSnapshot` API, so I'm using that here to
1960 + // simulate an error during hydration.
1961 + function getServerSnapshot() {
1962 + if (isClient) {
1963 + throw new Error('Hydration error');
1964 + }
1965 + return 'Yay!';
1966 + }
1967 +
1968 + function Child() {
1969 + const value = useSyncExternalStore(
1970 + subscribe,
1971 + getClientSnapshot,
1972 + getServerSnapshot,
1973 + );
1974 + if (isClient) {
1975 + readText(value);
1976 + }
1977 + Scheduler.unstable_yieldValue(value);
1978 + return value;
1979 + }
1980 +
1981 + const span1Ref = React.createRef();
1982 + const span2Ref = React.createRef();
1983 + const span3Ref = React.createRef();
1984 +
1985 + function App() {
1986 + return (
1987 + <div>
1988 + <span ref={span1Ref} />
1989 + <Suspense fallback="Loading...">
1990 + <span ref={span2Ref}>
1991 + <Child />
1992 + </span>
1993 + </Suspense>
1994 + <span ref={span3Ref} />
1995 + </div>
1996 + );
1997 + }
1998 +
1999 + await act(async () => {
2000 + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
2001 + <App />,
2002 + writable,
2003 + );
2004 + startWriting();
2005 + });
2006 + expect(Scheduler).toHaveYielded(['Yay!']);
2007 +
2008 + const [span1, span2, span3] = container.getElementsByTagName('span');
2009 +
2010 + // Hydrate the tree. Child will throw during hydration, but not when it
2011 + // falls back to client rendering.
2012 + isClient = true;
2013 + ReactDOM.hydrateRoot(container, <App />);
2014 +
2015 + expect(Scheduler).toFlushAndYield([]);
2016 + expect(getVisibleChildren(container)).toEqual(
2017 + <div>
2018 + <span />
2019 + Loading...
2020 + <span />
2021 + </div>,
2022 + );
2023 +
2024 + await act(async () => {
2025 + resolveText('Yay!');
2026 + });
2027 + expect(Scheduler).toFlushAndYield(['Yay!']);
2028 + expect(getVisibleChildren(container)).toEqual(
2029 + <div>
2030 + <span />
2031 + <span>Yay!</span>
2032 + <span />
2033 + </div>,
2034 + );
2035 +
2036 + // The node that's inside the boundary that errored during hydration was
2037 + // not hydrated.
2038 + expect(span2Ref.current).not.toBe(span2);
2039 +
2040 + // But the nodes outside the boundary were.
2041 + expect(span1Ref.current).toBe(span1);
2042 + expect(span3Ref.current).toBe(span3);
2043 + },
2044 + );
2045 });
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+9
@@ -74,6 +74,7 @@ import {
74 ForceUpdateForLegacySuspense,
75 StaticMask,
76 ShouldCapture,
77 + ForceClientRender,
78 } from './ReactFiberFlags';
79 import ReactSharedInternals from 'shared/ReactSharedInternals';
80 import {
@@ -2081,6 +2082,14 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2082 prevState,
2083 renderLanes,
2084 );
2085 + } else if (workInProgress.flags & ForceClientRender) {
2086 + // Something errored during hydration. Try again without hydrating.
2087 + workInProgress.flags &= ~ForceClientRender;
2088 + return retrySuspenseComponentWithoutHydrating(
2089 + current,
2090 + workInProgress,
2091 + renderLanes,
2092 + );
2093 } else if (
2094 (workInProgress.memoizedState: null | SuspenseState) !== null
2095 ) {
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+9
@@ -74,6 +74,7 @@ import {
74 ForceUpdateForLegacySuspense,
75 StaticMask,
76 ShouldCapture,
77 + ForceClientRender,
78 } from './ReactFiberFlags';
79 import ReactSharedInternals from 'shared/ReactSharedInternals';
80 import {
@@ -2081,6 +2082,14 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2082 prevState,
2083 renderLanes,
2084 );
2085 + } else if (workInProgress.flags & ForceClientRender) {
2086 + // Something errored during hydration. Try again without hydrating.
2087 + workInProgress.flags &= ~ForceClientRender;
2088 + return retrySuspenseComponentWithoutHydrating(
2089 + current,
2090 + workInProgress,
2091 + renderLanes,
2092 + );
2093 } else if (
2094 (workInProgress.memoizedState: null | SuspenseState) !== null
2095 ) {
packages/react-reconciler/src/ReactFiberFlags.js
+27 -26
@@ -12,53 +12,54 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
12 export type Flags = number;
13
14 // Don't change these two values. They're used by React Dev Tools.
15 -export const NoFlags = /* */ 0b000000000000000000000000;
16 -export const PerformedWork = /* */ 0b000000000000000000000001;
15 +export const NoFlags = /* */ 0b0000000000000000000000000;
16 +export const PerformedWork = /* */ 0b0000000000000000000000001;
17
18 // You can change the rest (and add more).
19 -export const Placement = /* */ 0b000000000000000000000010;
20 -export const Update = /* */ 0b000000000000000000000100;
19 +export const Placement = /* */ 0b0000000000000000000000010;
20 +export const Update = /* */ 0b0000000000000000000000100;
21 export const PlacementAndUpdate = /* */ Placement | Update;
22 -export const Deletion = /* */ 0b000000000000000000001000;
23 -export const ChildDeletion = /* */ 0b000000000000000000010000;
24 -export const ContentReset = /* */ 0b000000000000000000100000;
25 -export const Callback = /* */ 0b000000000000000001000000;
26 -export const DidCapture = /* */ 0b000000000000000010000000;
27 -export const Ref = /* */ 0b000000000000000100000000;
28 -export const Snapshot = /* */ 0b000000000000001000000000;
29 -export const Passive = /* */ 0b000000000000010000000000;
30 -export const Hydrating = /* */ 0b000000000000100000000000;
22 +export const Deletion = /* */ 0b0000000000000000000001000;
23 +export const ChildDeletion = /* */ 0b0000000000000000000010000;
24 +export const ContentReset = /* */ 0b0000000000000000000100000;
25 +export const Callback = /* */ 0b0000000000000000001000000;
26 +export const DidCapture = /* */ 0b0000000000000000010000000;
27 +export const ForceClientRender = /* */ 0b0000000000000000100000000;
28 +export const Ref = /* */ 0b0000000000000001000000000;
29 +export const Snapshot = /* */ 0b0000000000000010000000000;
30 +export const Passive = /* */ 0b0000000000000100000000000;
31 +export const Hydrating = /* */ 0b0000000000001000000000000;
32 export const HydratingAndUpdate = /* */ Hydrating | Update;
32 -export const Visibility = /* */ 0b000000000001000000000000;
33 -export const StoreConsistency = /* */ 0b000000000010000000000000;
33 +export const Visibility = /* */ 0b0000000000010000000000000;
34 +export const StoreConsistency = /* */ 0b0000000000100000000000000;
35
36 export const LifecycleEffectMask =
37 Passive | Update | Callback | Ref | Snapshot | StoreConsistency;
38
39 // Union of all commit flags (flags with the lifetime of a particular commit)
39 -export const HostEffectMask = /* */ 0b000000000011111111111111;
40 +export const HostEffectMask = /* */ 0b0000000000111111111111111;
41
42 // These are not really side effects, but we still reuse this field.
42 -export const Incomplete = /* */ 0b000000000100000000000000;
43 -export const ShouldCapture = /* */ 0b000000001000000000000000;
44 -export const ForceUpdateForLegacySuspense = /* */ 0b000000010000000000000000;
45 -export const DidPropagateContext = /* */ 0b000000100000000000000000;
46 -export const NeedsPropagation = /* */ 0b000001000000000000000000;
43 +export const Incomplete = /* */ 0b0000000001000000000000000;
44 +export const ShouldCapture = /* */ 0b0000000010000000000000000;
45 +export const ForceUpdateForLegacySuspense = /* */ 0b0000000100000000000000000;
46 +export const DidPropagateContext = /* */ 0b0000001000000000000000000;
47 +export const NeedsPropagation = /* */ 0b0000010000000000000000000;
48
49 // Static tags describe aspects of a fiber that are not specific to a render,
50 // e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
51 // This enables us to defer more work in the unmount case,
52 // since we can defer traversing the tree during layout to look for Passive effects,
53 // and instead rely on the static flag as a signal that there may be cleanup work.
53 -export const RefStatic = /* */ 0b000010000000000000000000;
54 -export const LayoutStatic = /* */ 0b000100000000000000000000;
55 -export const PassiveStatic = /* */ 0b001000000000000000000000;
54 +export const RefStatic = /* */ 0b0000100000000000000000000;
55 +export const LayoutStatic = /* */ 0b0001000000000000000000000;
56 +export const PassiveStatic = /* */ 0b0010000000000000000000000;
57
58 // These flags allow us to traverse to fibers that have effects on mount
59 // without traversing the entire tree after every commit for
60 // double invoking
60 -export const MountLayoutDev = /* */ 0b010000000000000000000000;
61 -export const MountPassiveDev = /* */ 0b100000000000000000000000;
61 +export const MountLayoutDev = /* */ 0b0100000000000000000000000;
62 +export const MountPassiveDev = /* */ 0b1000000000000000000000000;
63
64 // Groups of flags that are used in the commit phase to skip over trees that
65 // don't contain effects, by checking subtreeFlags.
packages/react-reconciler/src/ReactFiberThrow.new.js
+301 -222
@@ -32,6 +32,7 @@ import {
32 ShouldCapture,
33 LifecycleEffectMask,
34 ForceUpdateForLegacySuspense,
35 + ForceClientRender,
36 } from './ReactFiberFlags';
37 import {
38 supportsPersistence,
@@ -78,6 +79,7 @@ import {
79 mergeLanes,
80 pickArbitraryLane,
81 } from './ReactFiberLane.new';
82 +import {getIsHydrating} from './ReactFiberHydrationContext.new';
83
84 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
85
@@ -160,35 +162,264 @@ function createClassErrorUpdate(
162 return update;
163 }
164
163 -function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
164 - // Attach a listener to the promise to "ping" the root and retry. But only if
165 - // one does not already exist for the lanes we're currently rendering (which
166 - // acts like a "thread ID" here).
167 - let pingCache = root.pingCache;
168 - let threadIDs;
169 - if (pingCache === null) {
170 - pingCache = root.pingCache = new PossiblyWeakMap();
171 - threadIDs = new Set();
172 - pingCache.set(wakeable, threadIDs);
173 - } else {
174 - threadIDs = pingCache.get(wakeable);
175 - if (threadIDs === undefined) {
165 +function attachWakeableListeners(
166 + suspenseBoundary: Fiber,
167 + root: FiberRoot,
168 + wakeable: Wakeable,
169 + lanes: Lanes,
170 +) {
171 + // Attach a ping listener
172 + //
173 + // The data might resolve before we have a chance to commit the fallback. Or,
174 + // in the case of a refresh, we'll never commit a fallback. So we need to
175 + // attach a listener now. When it resolves ("pings"), we can decide whether to
176 + // try rendering the tree again.
177 + //
178 + // Only attach a listener if one does not already exist for the lanes
179 + // we're currently rendering (which acts like a "thread ID" here).
180 + //
181 + // We only need to do this in concurrent mode. Legacy Suspense always
182 + // commits fallbacks synchronously, so there are no pings.
183 + if (suspenseBoundary.mode & ConcurrentMode) {
184 + let pingCache = root.pingCache;
185 + let threadIDs;
186 + if (pingCache === null) {
187 + pingCache = root.pingCache = new PossiblyWeakMap();
188 threadIDs = new Set();
189 pingCache.set(wakeable, threadIDs);
190 + } else {
191 + threadIDs = pingCache.get(wakeable);
192 + if (threadIDs === undefined) {
193 + threadIDs = new Set();
194 + pingCache.set(wakeable, threadIDs);
195 + }
196 }
179 - }
180 - if (!threadIDs.has(lanes)) {
181 - // Memoize using the thread ID to prevent redundant listeners.
182 - threadIDs.add(lanes);
183 - const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
184 - if (enableUpdaterTracking) {
185 - if (isDevToolsPresent) {
186 - // If we have pending work still, restore the original updaters
187 - restorePendingUpdaters(root, lanes);
197 + if (!threadIDs.has(lanes)) {
198 + // Memoize using the thread ID to prevent redundant listeners.
199 + threadIDs.add(lanes);
200 + const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
201 + if (enableUpdaterTracking) {
202 + if (isDevToolsPresent) {
203 + // If we have pending work still, restore the original updaters
204 + restorePendingUpdaters(root, lanes);
205 + }
206 }
207 + wakeable.then(ping, ping);
208 }
190 - wakeable.then(ping, ping);
209 }
210 +
211 + // Retry listener
212 + //
213 + // If the fallback does commit, we need to attach a different type of
214 + // listener. This one schedules an update on the Suspense boundary to turn
215 + // the fallback state off.
216 + //
217 + // Stash the wakeable on the boundary fiber so we can access it in the
218 + // commit phase.
219 + //
220 + // When the wakeable resolves, we'll attempt to render the boundary
221 + // again ("retry").
222 + const wakeables: Set<Wakeable> | null = (suspenseBoundary.updateQueue: any);
223 + if (wakeables === null) {
224 + const updateQueue = (new Set(): any);
225 + updateQueue.add(wakeable);
226 + suspenseBoundary.updateQueue = updateQueue;
227 + } else {
228 + wakeables.add(wakeable);
229 + }
230 +}
231 +
232 +function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
233 + if (enableLazyContextPropagation) {
234 + const currentSourceFiber = sourceFiber.alternate;
235 + if (currentSourceFiber !== null) {
236 + // Since we never visited the children of the suspended component, we
237 + // need to propagate the context change now, to ensure that we visit
238 + // them during the retry.
239 + //
240 + // We don't have to do this for errors because we retry errors without
241 + // committing in between. So this is specific to Suspense.
242 + propagateParentContextChangesToDeferredTree(
243 + currentSourceFiber,
244 + sourceFiber,
245 + rootRenderLanes,
246 + );
247 + }
248 + }
249 +
250 + // Reset the memoizedState to what it was before we attempted to render it.
251 + // A legacy mode Suspense quirk, only relevant to hook components.
252 + const tag = sourceFiber.tag;
253 + if (
254 + (sourceFiber.mode & ConcurrentMode) === NoMode &&
255 + (tag === FunctionComponent ||
256 + tag === ForwardRef ||
257 + tag === SimpleMemoComponent)
258 + ) {
259 + const currentSource = sourceFiber.alternate;
260 + if (currentSource) {
261 + sourceFiber.updateQueue = currentSource.updateQueue;
262 + sourceFiber.memoizedState = currentSource.memoizedState;
263 + sourceFiber.lanes = currentSource.lanes;
264 + } else {
265 + sourceFiber.updateQueue = null;
266 + sourceFiber.memoizedState = null;
267 + }
268 + }
269 +}
270 +
271 +function markNearestSuspenseBoundaryShouldCapture(
272 + returnFiber: Fiber,
273 + sourceFiber: Fiber,
274 + root: FiberRoot,
275 + rootRenderLanes: Lanes,
276 +): Fiber | null {
277 + const hasInvisibleParentBoundary = hasSuspenseContext(
278 + suspenseStackCursor.current,
279 + (InvisibleParentSuspenseContext: SuspenseContext),
280 + );
281 + let node = returnFiber;
282 + do {
283 + if (
284 + node.tag === SuspenseComponent &&
285 + shouldCaptureSuspense(node, hasInvisibleParentBoundary)
286 + ) {
287 + // Found the nearest boundary.
288 + const suspenseBoundary = node;
289 +
290 + // This marks a Suspense boundary so that when we're unwinding the stack,
291 + // it captures the suspended "exception" and does a second (fallback) pass.
292 +
293 + if ((suspenseBoundary.mode & ConcurrentMode) === NoMode) {
294 + // Legacy Mode Suspense
295 + //
296 + // If the boundary is in legacy mode, we should *not*
297 + // suspend the commit. Pretend as if the suspended component rendered
298 + // null and keep rendering. When the Suspense boundary completes,
299 + // we'll do a second pass to render the fallback.
300 + if (suspenseBoundary === returnFiber) {
301 + // Special case where we suspended while reconciling the children of
302 + // a Suspense boundary's inner Offscreen wrapper fiber. This happens
303 + // when a React.lazy component is a direct child of a
304 + // Suspense boundary.
305 + //
306 + // Suspense boundaries are implemented as multiple fibers, but they
307 + // are a single conceptual unit. The legacy mode behavior where we
308 + // pretend the suspended fiber committed as `null` won't work,
309 + // because in this case the "suspended" fiber is the inner
310 + // Offscreen wrapper.
311 + //
312 + // Because the contents of the boundary haven't started rendering
313 + // yet (i.e. nothing in the tree has partially rendered) we can
314 + // switch to the regular, concurrent mode behavior: mark the
315 + // boundary with ShouldCapture and enter the unwind phase.
316 + suspenseBoundary.flags |= ShouldCapture;
317 + } else {
318 + suspenseBoundary.flags |= DidCapture;
319 + sourceFiber.flags |= ForceUpdateForLegacySuspense;
320 +
321 + // We're going to commit this fiber even though it didn't complete.
322 + // But we shouldn't call any lifecycle methods or callbacks. Remove
323 + // all lifecycle effect tags.
324 + sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete);
325 +
326 + if (supportsPersistence && enablePersistentOffscreenHostContainer) {
327 + // Another legacy Suspense quirk. In persistent mode, if this is the
328 + // initial mount, override the props of the host container to hide
329 + // its contents.
330 + const currentSuspenseBoundary = suspenseBoundary.alternate;
331 + if (currentSuspenseBoundary === null) {
332 + const offscreenFiber: Fiber = (suspenseBoundary.child: any);
333 + const offscreenContainer = offscreenFiber.child;
334 + if (offscreenContainer !== null) {
335 + const children = offscreenContainer.memoizedProps.children;
336 + const containerProps = getOffscreenContainerProps(
337 + 'hidden',
338 + children,
339 + );
340 + offscreenContainer.pendingProps = containerProps;
341 + offscreenContainer.memoizedProps = containerProps;
342 + }
343 + }
344 + }
345 +
346 + if (sourceFiber.tag === ClassComponent) {
347 + const currentSourceFiber = sourceFiber.alternate;
348 + if (currentSourceFiber === null) {
349 + // This is a new mount. Change the tag so it's not mistaken for a
350 + // completed class component. For example, we should not call
351 + // componentWillUnmount if it is deleted.
352 + sourceFiber.tag = IncompleteClassComponent;
353 + } else {
354 + // When we try rendering again, we should not reuse the current fiber,
355 + // since it's known to be in an inconsistent state. Use a force update to
356 + // prevent a bail out.
357 + const update = createUpdate(NoTimestamp, SyncLane);
358 + update.tag = ForceUpdate;
359 + enqueueUpdate(sourceFiber, update, SyncLane);
360 + }
361 + }
362 +
363 + // The source fiber did not complete. Mark it with Sync priority to
364 + // indicate that it still has pending work.
365 + sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane);
366 + }
367 + return suspenseBoundary;
368 + }
369 + // Confirmed that the boundary is in a concurrent mode tree. Continue
370 + // with the normal suspend path.
371 + //
372 + // After this we'll use a set of heuristics to determine whether this
373 + // render pass will run to completion or restart or "suspend" the commit.
374 + // The actual logic for this is spread out in different places.
375 + //
376 + // This first principle is that if we're going to suspend when we complete
377 + // a root, then we should also restart if we get an update or ping that
378 + // might unsuspend it, and vice versa. The only reason to suspend is
379 + // because you think you might want to restart before committing. However,
380 + // it doesn't make sense to restart only while in the period we're suspended.
381 + //
382 + // Restarting too aggressively is also not good because it starves out any
383 + // intermediate loading state. So we use heuristics to determine when.
384 +
385 + // Suspense Heuristics
386 + //
387 + // If nothing threw a Promise or all the same fallbacks are already showing,
388 + // then don't suspend/restart.
389 + //
390 + // If this is an initial render of a new tree of Suspense boundaries and
391 + // those trigger a fallback, then don't suspend/restart. We want to ensure
392 + // that we can show the initial loading state as quickly as possible.
393 + //
394 + // If we hit a "Delayed" case, such as when we'd switch from content back into
395 + // a fallback, then we should always suspend/restart. Transitions apply
396 + // to this case. If none is defined, JND is used instead.
397 + //
398 + // If we're already showing a fallback and it gets "retried", allowing us to show
399 + // another level, but there's still an inner boundary that would show a fallback,
400 + // then we suspend/restart for 500ms since the last time we showed a fallback
401 + // anywhere in the tree. This effectively throttles progressive loading into a
402 + // consistent train of commits. This also gives us an opportunity to restart to
403 + // get to the completed state slightly earlier.
404 + //
405 + // If there's ambiguity due to batching it's resolved in preference of:
406 + // 1) "delayed", 2) "initial render", 3) "retry".
407 + //
408 + // We want to ensure that a "busy" state doesn't get force committed. We want to
409 + // ensure that new initial loading states can commit as soon as possible.
410 + suspenseBoundary.flags |= ShouldCapture;
411 + // TODO: I think we can remove this, since we now use `DidCapture` in
412 + // the begin phase to prevent an early bailout.
413 + suspenseBoundary.lanes = rootRenderLanes;
414 + return suspenseBoundary;
415 + }
416 + // This boundary already captured during this render. Continue to the next
417 + // boundary.
418 + node = node.return;
419 + } while (node !== null);
420 +
421 + // Could not find a Suspense boundary capable of capturing.
422 + return null;
423 }
424
425 function throwException(
@@ -213,25 +444,9 @@ function throwException(
444 typeof value === 'object' &&
445 typeof value.then === 'function'
446 ) {
216 - if (enableLazyContextPropagation) {
217 - const currentSourceFiber = sourceFiber.alternate;
218 - if (currentSourceFiber !== null) {
219 - // Since we never visited the children of the suspended component, we
220 - // need to propagate the context change now, to ensure that we visit
221 - // them during the retry.
222 - //
223 - // We don't have to do this for errors because we retry errors without
224 - // committing in between. So this is specific to Suspense.
225 - propagateParentContextChangesToDeferredTree(
226 - currentSourceFiber,
227 - sourceFiber,
228 - rootRenderLanes,
229 - );
230 - }
231 - }
232 -
233 - // This is a wakeable.
447 + // This is a wakeable. The component suspended.
448 const wakeable: Wakeable = (value: any);
449 + resetSuspendedComponent(sourceFiber, rootRenderLanes);
450
451 if (__DEV__) {
452 if (enableDebugTracing) {
@@ -242,190 +457,54 @@ function throwException(
457 }
458 }
459
245 - // Reset the memoizedState to what it was before we attempted to render it.
246 - // A legacy mode Suspense quirk, only relevant to hook components.
247 - const tag = sourceFiber.tag;
248 - if (
249 - (sourceFiber.mode & ConcurrentMode) === NoMode &&
250 - (tag === FunctionComponent ||
251 - tag === ForwardRef ||
252 - tag === SimpleMemoComponent)
253 - ) {
254 - const currentSource = sourceFiber.alternate;
255 - if (currentSource) {
256 - sourceFiber.updateQueue = currentSource.updateQueue;
257 - sourceFiber.memoizedState = currentSource.memoizedState;
258 - sourceFiber.lanes = currentSource.lanes;
259 - } else {
260 - sourceFiber.updateQueue = null;
261 - sourceFiber.memoizedState = null;
262 - }
263 - }
264 -
265 - const hasInvisibleParentBoundary = hasSuspenseContext(
266 - suspenseStackCursor.current,
267 - (InvisibleParentSuspenseContext: SuspenseContext),
268 - );
269 -
460 // Schedule the nearest Suspense to re-render the timed out view.
271 - let workInProgress = returnFiber;
272 - do {
273 - if (
274 - workInProgress.tag === SuspenseComponent &&
275 - shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary)
276 - ) {
277 - // Found the nearest boundary.
278 -
279 - // Stash the promise on the boundary fiber. If the boundary times out, we'll
280 - // attach another listener to flip the boundary back to its normal state.
281 - const wakeables: Set<Wakeable> = (workInProgress.updateQueue: any);
282 - if (wakeables === null) {
283 - const updateQueue = (new Set(): any);
284 - updateQueue.add(wakeable);
285 - workInProgress.updateQueue = updateQueue;
286 - } else {
287 - wakeables.add(wakeable);
288 - }
289 -
290 - if ((workInProgress.mode & ConcurrentMode) === NoMode) {
291 - // Legacy Mode Suspense
292 - //
293 - // If the boundary is in legacy mode, we should *not*
294 - // suspend the commit. Pretend as if the suspended component rendered
295 - // null and keep rendering. When the Suspense boundary completes,
296 - // we'll do a second pass to render the fallback.
297 - if (workInProgress === returnFiber) {
298 - // Special case where we suspended while reconciling the children of
299 - // a Suspense boundary's inner Offscreen wrapper fiber. This happens
300 - // when a React.lazy component is a direct child of a
301 - // Suspense boundary.
302 - //
303 - // Suspense boundaries are implemented as multiple fibers, but they
304 - // are a single conceptual unit. The legacy mode behavior where we
305 - // pretend the suspended fiber committed as `null` won't work,
306 - // because in this case the "suspended" fiber is the inner
307 - // Offscreen wrapper.
308 - //
309 - // Because the contents of the boundary haven't started rendering
310 - // yet (i.e. nothing in the tree has partially rendered) we can
311 - // switch to the regular, concurrent mode behavior: mark the
312 - // boundary with ShouldCapture and enter the unwind phase.
313 - workInProgress.flags |= ShouldCapture;
314 - } else {
315 - workInProgress.flags |= DidCapture;
316 - sourceFiber.flags |= ForceUpdateForLegacySuspense;
317 -
318 - // We're going to commit this fiber even though it didn't complete.
319 - // But we shouldn't call any lifecycle methods or callbacks. Remove
320 - // all lifecycle effect tags.
321 - sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete);
322 -
323 - if (supportsPersistence && enablePersistentOffscreenHostContainer) {
324 - // Another legacy Suspense quirk. In persistent mode, if this is the
325 - // initial mount, override the props of the host container to hide
326 - // its contents.
327 - const currentSuspenseBoundary = workInProgress.alternate;
328 - if (currentSuspenseBoundary === null) {
329 - const offscreenFiber: Fiber = (workInProgress.child: any);
330 - const offscreenContainer = offscreenFiber.child;
331 - if (offscreenContainer !== null) {
332 - const children = offscreenContainer.memoizedProps.children;
333 - const containerProps = getOffscreenContainerProps(
334 - 'hidden',
335 - children,
336 - );
337 - offscreenContainer.pendingProps = containerProps;
338 - offscreenContainer.memoizedProps = containerProps;
339 - }
340 - }
341 - }
342 -
343 - if (sourceFiber.tag === ClassComponent) {
344 - const currentSourceFiber = sourceFiber.alternate;
345 - if (currentSourceFiber === null) {
346 - // This is a new mount. Change the tag so it's not mistaken for a
347 - // completed class component. For example, we should not call
348 - // componentWillUnmount if it is deleted.
349 - sourceFiber.tag = IncompleteClassComponent;
350 - } else {
351 - // When we try rendering again, we should not reuse the current fiber,
352 - // since it's known to be in an inconsistent state. Use a force update to
353 - // prevent a bail out.
354 - const update = createUpdate(NoTimestamp, SyncLane);
355 - update.tag = ForceUpdate;
356 - enqueueUpdate(sourceFiber, update, SyncLane);
357 - }
358 - }
359 -
360 - // The source fiber did not complete. Mark it with Sync priority to
361 - // indicate that it still has pending work.
362 - sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane);
363 - }
364 - return;
365 - }
366 - // Confirmed that the boundary is in a concurrent mode tree. Continue
367 - // with the normal suspend path.
368 - //
369 - // After this we'll use a set of heuristics to determine whether this
370 - // render pass will run to completion or restart or "suspend" the commit.
371 - // The actual logic for this is spread out in different places.
372 - //
373 - // This first principle is that if we're going to suspend when we complete
374 - // a root, then we should also restart if we get an update or ping that
375 - // might unsuspend it, and vice versa. The only reason to suspend is
376 - // because you think you might want to restart before committing. However,
377 - // it doesn't make sense to restart only while in the period we're suspended.
378 - //
379 - // Restarting too aggressively is also not good because it starves out any
380 - // intermediate loading state. So we use heuristics to determine when.
381 -
382 - // Suspense Heuristics
383 - //
384 - // If nothing threw a Promise or all the same fallbacks are already showing,
385 - // then don't suspend/restart.
386 - //
387 - // If this is an initial render of a new tree of Suspense boundaries and
388 - // those trigger a fallback, then don't suspend/restart. We want to ensure
389 - // that we can show the initial loading state as quickly as possible.
390 - //
391 - // If we hit a "Delayed" case, such as when we'd switch from content back into
392 - // a fallback, then we should always suspend/restart. Transitions apply
393 - // to this case. If none is defined, JND is used instead.
394 - //
395 - // If we're already showing a fallback and it gets "retried", allowing us to show
396 - // another level, but there's still an inner boundary that would show a fallback,
397 - // then we suspend/restart for 500ms since the last time we showed a fallback
398 - // anywhere in the tree. This effectively throttles progressive loading into a
399 - // consistent train of commits. This also gives us an opportunity to restart to
400 - // get to the completed state slightly earlier.
401 - //
402 - // If there's ambiguity due to batching it's resolved in preference of:
403 - // 1) "delayed", 2) "initial render", 3) "retry".
404 - //
405 - // We want to ensure that a "busy" state doesn't get force committed. We want to
406 - // ensure that new initial loading states can commit as soon as possible.
407 -
408 - attachPingListener(root, wakeable, rootRenderLanes);
409 -
410 - workInProgress.flags |= ShouldCapture;
411 - // TODO: I think we can remove this, since we now use `DidCapture` in
412 - // the begin phase to prevent an early bailout.
413 - workInProgress.lanes = rootRenderLanes;
461 + const suspenseBoundary = markNearestSuspenseBoundaryShouldCapture(
462 + returnFiber,
463 + sourceFiber,
464 + root,
465 + rootRenderLanes,
466 + );
467 + if (suspenseBoundary !== null) {
468 + attachWakeableListeners(
469 + suspenseBoundary,
470 + root,
471 + wakeable,
472 + rootRenderLanes,
473 + );
474 + return;
475 + } else {
476 + // No boundary was found. Fallthrough to error mode.
477 + // TODO: Use invariant so the message is stripped in prod?
478 + value = new Error(
479 + (getComponentNameFromFiber(sourceFiber) || 'A React component') +
480 + ' suspended while rendering, but no fallback UI was specified.\n' +
481 + '\n' +
482 + 'Add a <Suspense fallback=...> component higher in the tree to ' +
483 + 'provide a loading indicator or placeholder to display.',
484 + );
485 + }
486 + } else {
487 + // This is a regular error, not a Suspense wakeable.
488 + if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
489 + // If the error was thrown during hydration, we may be able to recover by
490 + // discarding the dehydrated content and switching to a client render.
491 + // Instead of surfacing the error, find the nearest Suspense boundary
492 + // and render it again without hydration.
493 + const suspenseBoundary = markNearestSuspenseBoundaryShouldCapture(
494 + returnFiber,
495 + sourceFiber,
496 + root,
497 + rootRenderLanes,
498 + );
499 + if (suspenseBoundary !== null) {
500 + // Set a flag to indicate that we should try rendering the normal
501 + // children again, not the fallback.
502 + suspenseBoundary.flags |= ForceClientRender;
503 return;
504 }
416 - // This boundary already captured during this render. Continue to the next
417 - // boundary.
418 - workInProgress = workInProgress.return;
419 - } while (workInProgress !== null);
420 - // No boundary was found. Fallthrough to error mode.
421 - // TODO: Use invariant so the message is stripped in prod?
422 - value = new Error(
423 - (getComponentNameFromFiber(sourceFiber) || 'A React component') +
424 - ' suspended while rendering, but no fallback UI was specified.\n' +
425 - '\n' +
426 - 'Add a <Suspense fallback=...> component higher in the tree to ' +
427 - 'provide a loading indicator or placeholder to display.',
428 - );
505 + } else {
506 + // Otherwise, fall through to the error path.
507 + }
508 }
509
510 // We didn't find a boundary that could handle this type of exception. Start
packages/react-reconciler/src/ReactFiberThrow.old.js
+301 -222
@@ -32,6 +32,7 @@ import {
32 ShouldCapture,
33 LifecycleEffectMask,
34 ForceUpdateForLegacySuspense,
35 + ForceClientRender,
36 } from './ReactFiberFlags';
37 import {
38 supportsPersistence,
@@ -78,6 +79,7 @@ import {
79 mergeLanes,
80 pickArbitraryLane,
81 } from './ReactFiberLane.old';
82 +import {getIsHydrating} from './ReactFiberHydrationContext.old';
83
84 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
85
@@ -160,35 +162,264 @@ function createClassErrorUpdate(
162 return update;
163 }
164
163 -function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
164 - // Attach a listener to the promise to "ping" the root and retry. But only if
165 - // one does not already exist for the lanes we're currently rendering (which
166 - // acts like a "thread ID" here).
167 - let pingCache = root.pingCache;
168 - let threadIDs;
169 - if (pingCache === null) {
170 - pingCache = root.pingCache = new PossiblyWeakMap();
171 - threadIDs = new Set();
172 - pingCache.set(wakeable, threadIDs);
173 - } else {
174 - threadIDs = pingCache.get(wakeable);
175 - if (threadIDs === undefined) {
165 +function attachWakeableListeners(
166 + suspenseBoundary: Fiber,
167 + root: FiberRoot,
168 + wakeable: Wakeable,
169 + lanes: Lanes,
170 +) {
171 + // Attach a ping listener
172 + //
173 + // The data might resolve before we have a chance to commit the fallback. Or,
174 + // in the case of a refresh, we'll never commit a fallback. So we need to
175 + // attach a listener now. When it resolves ("pings"), we can decide whether to
176 + // try rendering the tree again.
177 + //
178 + // Only attach a listener if one does not already exist for the lanes
179 + // we're currently rendering (which acts like a "thread ID" here).
180 + //
181 + // We only need to do this in concurrent mode. Legacy Suspense always
182 + // commits fallbacks synchronously, so there are no pings.
183 + if (suspenseBoundary.mode & ConcurrentMode) {
184 + let pingCache = root.pingCache;
185 + let threadIDs;
186 + if (pingCache === null) {
187 + pingCache = root.pingCache = new PossiblyWeakMap();
188 threadIDs = new Set();
189 pingCache.set(wakeable, threadIDs);
190 + } else {
191 + threadIDs = pingCache.get(wakeable);
192 + if (threadIDs === undefined) {
193 + threadIDs = new Set();
194 + pingCache.set(wakeable, threadIDs);
195 + }
196 }
179 - }
180 - if (!threadIDs.has(lanes)) {
181 - // Memoize using the thread ID to prevent redundant listeners.
182 - threadIDs.add(lanes);
183 - const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
184 - if (enableUpdaterTracking) {
185 - if (isDevToolsPresent) {
186 - // If we have pending work still, restore the original updaters
187 - restorePendingUpdaters(root, lanes);
197 + if (!threadIDs.has(lanes)) {
198 + // Memoize using the thread ID to prevent redundant listeners.
199 + threadIDs.add(lanes);
200 + const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
201 + if (enableUpdaterTracking) {
202 + if (isDevToolsPresent) {
203 + // If we have pending work still, restore the original updaters
204 + restorePendingUpdaters(root, lanes);
205 + }
206 }
207 + wakeable.then(ping, ping);
208 }
190 - wakeable.then(ping, ping);
209 }
210 +
211 + // Retry listener
212 + //
213 + // If the fallback does commit, we need to attach a different type of
214 + // listener. This one schedules an update on the Suspense boundary to turn
215 + // the fallback state off.
216 + //
217 + // Stash the wakeable on the boundary fiber so we can access it in the
218 + // commit phase.
219 + //
220 + // When the wakeable resolves, we'll attempt to render the boundary
221 + // again ("retry").
222 + const wakeables: Set<Wakeable> | null = (suspenseBoundary.updateQueue: any);
223 + if (wakeables === null) {
224 + const updateQueue = (new Set(): any);
225 + updateQueue.add(wakeable);
226 + suspenseBoundary.updateQueue = updateQueue;
227 + } else {
228 + wakeables.add(wakeable);
229 + }
230 +}
231 +
232 +function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
233 + if (enableLazyContextPropagation) {
234 + const currentSourceFiber = sourceFiber.alternate;
235 + if (currentSourceFiber !== null) {
236 + // Since we never visited the children of the suspended component, we
237 + // need to propagate the context change now, to ensure that we visit
238 + // them during the retry.
239 + //
240 + // We don't have to do this for errors because we retry errors without
241 + // committing in between. So this is specific to Suspense.
242 + propagateParentContextChangesToDeferredTree(
243 + currentSourceFiber,
244 + sourceFiber,
245 + rootRenderLanes,
246 + );
247 + }
248 + }
249 +
250 + // Reset the memoizedState to what it was before we attempted to render it.
251 + // A legacy mode Suspense quirk, only relevant to hook components.
252 + const tag = sourceFiber.tag;
253 + if (
254 + (sourceFiber.mode & ConcurrentMode) === NoMode &&
255 + (tag === FunctionComponent ||
256 + tag === ForwardRef ||
257 + tag === SimpleMemoComponent)
258 + ) {
259 + const currentSource = sourceFiber.alternate;
260 + if (currentSource) {
261 + sourceFiber.updateQueue = currentSource.updateQueue;
262 + sourceFiber.memoizedState = currentSource.memoizedState;
263 + sourceFiber.lanes = currentSource.lanes;
264 + } else {
265 + sourceFiber.updateQueue = null;
266 + sourceFiber.memoizedState = null;
267 + }
268 + }
269 +}
270 +
271 +function markNearestSuspenseBoundaryShouldCapture(
272 + returnFiber: Fiber,
273 + sourceFiber: Fiber,
274 + root: FiberRoot,
275 + rootRenderLanes: Lanes,
276 +): Fiber | null {
277 + const hasInvisibleParentBoundary = hasSuspenseContext(
278 + suspenseStackCursor.current,
279 + (InvisibleParentSuspenseContext: SuspenseContext),
280 + );
281 + let node = returnFiber;
282 + do {
283 + if (
284 + node.tag === SuspenseComponent &&
285 + shouldCaptureSuspense(node, hasInvisibleParentBoundary)
286 + ) {
287 + // Found the nearest boundary.
288 + const suspenseBoundary = node;
289 +
290 + // This marks a Suspense boundary so that when we're unwinding the stack,
291 + // it captures the suspended "exception" and does a second (fallback) pass.
292 +
293 + if ((suspenseBoundary.mode & ConcurrentMode) === NoMode) {
294 + // Legacy Mode Suspense
295 + //
296 + // If the boundary is in legacy mode, we should *not*
297 + // suspend the commit. Pretend as if the suspended component rendered
298 + // null and keep rendering. When the Suspense boundary completes,
299 + // we'll do a second pass to render the fallback.
300 + if (suspenseBoundary === returnFiber) {
301 + // Special case where we suspended while reconciling the children of
302 + // a Suspense boundary's inner Offscreen wrapper fiber. This happens
303 + // when a React.lazy component is a direct child of a
304 + // Suspense boundary.
305 + //
306 + // Suspense boundaries are implemented as multiple fibers, but they
307 + // are a single conceptual unit. The legacy mode behavior where we
308 + // pretend the suspended fiber committed as `null` won't work,
309 + // because in this case the "suspended" fiber is the inner
310 + // Offscreen wrapper.
311 + //
312 + // Because the contents of the boundary haven't started rendering
313 + // yet (i.e. nothing in the tree has partially rendered) we can
314 + // switch to the regular, concurrent mode behavior: mark the
315 + // boundary with ShouldCapture and enter the unwind phase.
316 + suspenseBoundary.flags |= ShouldCapture;
317 + } else {
318 + suspenseBoundary.flags |= DidCapture;
319 + sourceFiber.flags |= ForceUpdateForLegacySuspense;
320 +
321 + // We're going to commit this fiber even though it didn't complete.
322 + // But we shouldn't call any lifecycle methods or callbacks. Remove
323 + // all lifecycle effect tags.
324 + sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete);
325 +
326 + if (supportsPersistence && enablePersistentOffscreenHostContainer) {
327 + // Another legacy Suspense quirk. In persistent mode, if this is the
328 + // initial mount, override the props of the host container to hide
329 + // its contents.
330 + const currentSuspenseBoundary = suspenseBoundary.alternate;
331 + if (currentSuspenseBoundary === null) {
332 + const offscreenFiber: Fiber = (suspenseBoundary.child: any);
333 + const offscreenContainer = offscreenFiber.child;
334 + if (offscreenContainer !== null) {
335 + const children = offscreenContainer.memoizedProps.children;
336 + const containerProps = getOffscreenContainerProps(
337 + 'hidden',
338 + children,
339 + );
340 + offscreenContainer.pendingProps = containerProps;
341 + offscreenContainer.memoizedProps = containerProps;
342 + }
343 + }
344 + }
345 +
346 + if (sourceFiber.tag === ClassComponent) {
347 + const currentSourceFiber = sourceFiber.alternate;
348 + if (currentSourceFiber === null) {
349 + // This is a new mount. Change the tag so it's not mistaken for a
350 + // completed class component. For example, we should not call
351 + // componentWillUnmount if it is deleted.
352 + sourceFiber.tag = IncompleteClassComponent;
353 + } else {
354 + // When we try rendering again, we should not reuse the current fiber,
355 + // since it's known to be in an inconsistent state. Use a force update to
356 + // prevent a bail out.
357 + const update = createUpdate(NoTimestamp, SyncLane);
358 + update.tag = ForceUpdate;
359 + enqueueUpdate(sourceFiber, update, SyncLane);
360 + }
361 + }
362 +
363 + // The source fiber did not complete. Mark it with Sync priority to
364 + // indicate that it still has pending work.
365 + sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane);
366 + }
367 + return suspenseBoundary;
368 + }
369 + // Confirmed that the boundary is in a concurrent mode tree. Continue
370 + // with the normal suspend path.
371 + //
372 + // After this we'll use a set of heuristics to determine whether this
373 + // render pass will run to completion or restart or "suspend" the commit.
374 + // The actual logic for this is spread out in different places.
375 + //
376 + // This first principle is that if we're going to suspend when we complete
377 + // a root, then we should also restart if we get an update or ping that
378 + // might unsuspend it, and vice versa. The only reason to suspend is
379 + // because you think you might want to restart before committing. However,
380 + // it doesn't make sense to restart only while in the period we're suspended.
381 + //
382 + // Restarting too aggressively is also not good because it starves out any
383 + // intermediate loading state. So we use heuristics to determine when.
384 +
385 + // Suspense Heuristics
386 + //
387 + // If nothing threw a Promise or all the same fallbacks are already showing,
388 + // then don't suspend/restart.
389 + //
390 + // If this is an initial render of a new tree of Suspense boundaries and
391 + // those trigger a fallback, then don't suspend/restart. We want to ensure
392 + // that we can show the initial loading state as quickly as possible.
393 + //
394 + // If we hit a "Delayed" case, such as when we'd switch from content back into
395 + // a fallback, then we should always suspend/restart. Transitions apply
396 + // to this case. If none is defined, JND is used instead.
397 + //
398 + // If we're already showing a fallback and it gets "retried", allowing us to show
399 + // another level, but there's still an inner boundary that would show a fallback,
400 + // then we suspend/restart for 500ms since the last time we showed a fallback
401 + // anywhere in the tree. This effectively throttles progressive loading into a
402 + // consistent train of commits. This also gives us an opportunity to restart to
403 + // get to the completed state slightly earlier.
404 + //
405 + // If there's ambiguity due to batching it's resolved in preference of:
406 + // 1) "delayed", 2) "initial render", 3) "retry".
407 + //
408 + // We want to ensure that a "busy" state doesn't get force committed. We want to
409 + // ensure that new initial loading states can commit as soon as possible.
410 + suspenseBoundary.flags |= ShouldCapture;
411 + // TODO: I think we can remove this, since we now use `DidCapture` in
412 + // the begin phase to prevent an early bailout.
413 + suspenseBoundary.lanes = rootRenderLanes;
414 + return suspenseBoundary;
415 + }
416 + // This boundary already captured during this render. Continue to the next
417 + // boundary.
418 + node = node.return;
419 + } while (node !== null);
420 +
421 + // Could not find a Suspense boundary capable of capturing.
422 + return null;
423 }
424
425 function throwException(
@@ -213,25 +444,9 @@ function throwException(
444 typeof value === 'object' &&
445 typeof value.then === 'function'
446 ) {
216 - if (enableLazyContextPropagation) {
217 - const currentSourceFiber = sourceFiber.alternate;
218 - if (currentSourceFiber !== null) {
219 - // Since we never visited the children of the suspended component, we
220 - // need to propagate the context change now, to ensure that we visit
221 - // them during the retry.
222 - //
223 - // We don't have to do this for errors because we retry errors without
224 - // committing in between. So this is specific to Suspense.
225 - propagateParentContextChangesToDeferredTree(
226 - currentSourceFiber,
227 - sourceFiber,
228 - rootRenderLanes,
229 - );
230 - }
231 - }
232 -
233 - // This is a wakeable.
447 + // This is a wakeable. The component suspended.
448 const wakeable: Wakeable = (value: any);
449 + resetSuspendedComponent(sourceFiber, rootRenderLanes);
450
451 if (__DEV__) {
452 if (enableDebugTracing) {
@@ -242,190 +457,54 @@ function throwException(
457 }
458 }
459
245 - // Reset the memoizedState to what it was before we attempted to render it.
246 - // A legacy mode Suspense quirk, only relevant to hook components.
247 - const tag = sourceFiber.tag;
248 - if (
249 - (sourceFiber.mode & ConcurrentMode) === NoMode &&
250 - (tag === FunctionComponent ||
251 - tag === ForwardRef ||
252 - tag === SimpleMemoComponent)
253 - ) {
254 - const currentSource = sourceFiber.alternate;
255 - if (currentSource) {
256 - sourceFiber.updateQueue = currentSource.updateQueue;
257 - sourceFiber.memoizedState = currentSource.memoizedState;
258 - sourceFiber.lanes = currentSource.lanes;
259 - } else {
260 - sourceFiber.updateQueue = null;
261 - sourceFiber.memoizedState = null;
262 - }
263 - }
264 -
265 - const hasInvisibleParentBoundary = hasSuspenseContext(
266 - suspenseStackCursor.current,
267 - (InvisibleParentSuspenseContext: SuspenseContext),
268 - );
269 -
460 // Schedule the nearest Suspense to re-render the timed out view.
271 - let workInProgress = returnFiber;
272 - do {
273 - if (
274 - workInProgress.tag === SuspenseComponent &&
275 - shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary)
276 - ) {
277 - // Found the nearest boundary.
278 -
279 - // Stash the promise on the boundary fiber. If the boundary times out, we'll
280 - // attach another listener to flip the boundary back to its normal state.
281 - const wakeables: Set<Wakeable> = (workInProgress.updateQueue: any);
282 - if (wakeables === null) {
283 - const updateQueue = (new Set(): any);
284 - updateQueue.add(wakeable);
285 - workInProgress.updateQueue = updateQueue;
286 - } else {
287 - wakeables.add(wakeable);
288 - }
289 -
290 - if ((workInProgress.mode & ConcurrentMode) === NoMode) {
291 - // Legacy Mode Suspense
292 - //
293 - // If the boundary is in legacy mode, we should *not*
294 - // suspend the commit. Pretend as if the suspended component rendered
295 - // null and keep rendering. When the Suspense boundary completes,
296 - // we'll do a second pass to render the fallback.
297 - if (workInProgress === returnFiber) {
298 - // Special case where we suspended while reconciling the children of
299 - // a Suspense boundary's inner Offscreen wrapper fiber. This happens
300 - // when a React.lazy component is a direct child of a
301 - // Suspense boundary.
302 - //
303 - // Suspense boundaries are implemented as multiple fibers, but they
304 - // are a single conceptual unit. The legacy mode behavior where we
305 - // pretend the suspended fiber committed as `null` won't work,
306 - // because in this case the "suspended" fiber is the inner
307 - // Offscreen wrapper.
308 - //
309 - // Because the contents of the boundary haven't started rendering
310 - // yet (i.e. nothing in the tree has partially rendered) we can
311 - // switch to the regular, concurrent mode behavior: mark the
312 - // boundary with ShouldCapture and enter the unwind phase.
313 - workInProgress.flags |= ShouldCapture;
314 - } else {
315 - workInProgress.flags |= DidCapture;
316 - sourceFiber.flags |= ForceUpdateForLegacySuspense;
317 -
318 - // We're going to commit this fiber even though it didn't complete.
319 - // But we shouldn't call any lifecycle methods or callbacks. Remove
320 - // all lifecycle effect tags.
321 - sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete);
322 -
323 - if (supportsPersistence && enablePersistentOffscreenHostContainer) {
324 - // Another legacy Suspense quirk. In persistent mode, if this is the
325 - // initial mount, override the props of the host container to hide
326 - // its contents.
327 - const currentSuspenseBoundary = workInProgress.alternate;
328 - if (currentSuspenseBoundary === null) {
329 - const offscreenFiber: Fiber = (workInProgress.child: any);
330 - const offscreenContainer = offscreenFiber.child;
331 - if (offscreenContainer !== null) {
332 - const children = offscreenContainer.memoizedProps.children;
333 - const containerProps = getOffscreenContainerProps(
334 - 'hidden',
335 - children,
336 - );
337 - offscreenContainer.pendingProps = containerProps;
338 - offscreenContainer.memoizedProps = containerProps;
339 - }
340 - }
341 - }
342 -
343 - if (sourceFiber.tag === ClassComponent) {
344 - const currentSourceFiber = sourceFiber.alternate;
345 - if (currentSourceFiber === null) {
346 - // This is a new mount. Change the tag so it's not mistaken for a
347 - // completed class component. For example, we should not call
348 - // componentWillUnmount if it is deleted.
349 - sourceFiber.tag = IncompleteClassComponent;
350 - } else {
351 - // When we try rendering again, we should not reuse the current fiber,
352 - // since it's known to be in an inconsistent state. Use a force update to
353 - // prevent a bail out.
354 - const update = createUpdate(NoTimestamp, SyncLane);
355 - update.tag = ForceUpdate;
356 - enqueueUpdate(sourceFiber, update, SyncLane);
357 - }
358 - }
359 -
360 - // The source fiber did not complete. Mark it with Sync priority to
361 - // indicate that it still has pending work.
362 - sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane);
363 - }
364 - return;
365 - }
366 - // Confirmed that the boundary is in a concurrent mode tree. Continue
367 - // with the normal suspend path.
368 - //
369 - // After this we'll use a set of heuristics to determine whether this
370 - // render pass will run to completion or restart or "suspend" the commit.
371 - // The actual logic for this is spread out in different places.
372 - //
373 - // This first principle is that if we're going to suspend when we complete
374 - // a root, then we should also restart if we get an update or ping that
375 - // might unsuspend it, and vice versa. The only reason to suspend is
376 - // because you think you might want to restart before committing. However,
377 - // it doesn't make sense to restart only while in the period we're suspended.
378 - //
379 - // Restarting too aggressively is also not good because it starves out any
380 - // intermediate loading state. So we use heuristics to determine when.
381 -
382 - // Suspense Heuristics
383 - //
384 - // If nothing threw a Promise or all the same fallbacks are already showing,
385 - // then don't suspend/restart.
386 - //
387 - // If this is an initial render of a new tree of Suspense boundaries and
388 - // those trigger a fallback, then don't suspend/restart. We want to ensure
389 - // that we can show the initial loading state as quickly as possible.
390 - //
391 - // If we hit a "Delayed" case, such as when we'd switch from content back into
392 - // a fallback, then we should always suspend/restart. Transitions apply
393 - // to this case. If none is defined, JND is used instead.
394 - //
395 - // If we're already showing a fallback and it gets "retried", allowing us to show
396 - // another level, but there's still an inner boundary that would show a fallback,
397 - // then we suspend/restart for 500ms since the last time we showed a fallback
398 - // anywhere in the tree. This effectively throttles progressive loading into a
399 - // consistent train of commits. This also gives us an opportunity to restart to
400 - // get to the completed state slightly earlier.
401 - //
402 - // If there's ambiguity due to batching it's resolved in preference of:
403 - // 1) "delayed", 2) "initial render", 3) "retry".
404 - //
405 - // We want to ensure that a "busy" state doesn't get force committed. We want to
406 - // ensure that new initial loading states can commit as soon as possible.
407 -
408 - attachPingListener(root, wakeable, rootRenderLanes);
409 -
410 - workInProgress.flags |= ShouldCapture;
411 - // TODO: I think we can remove this, since we now use `DidCapture` in
412 - // the begin phase to prevent an early bailout.
413 - workInProgress.lanes = rootRenderLanes;
461 + const suspenseBoundary = markNearestSuspenseBoundaryShouldCapture(
462 + returnFiber,
463 + sourceFiber,
464 + root,
465 + rootRenderLanes,
466 + );
467 + if (suspenseBoundary !== null) {
468 + attachWakeableListeners(
469 + suspenseBoundary,
470 + root,
471 + wakeable,
472 + rootRenderLanes,
473 + );
474 + return;
475 + } else {
476 + // No boundary was found. Fallthrough to error mode.
477 + // TODO: Use invariant so the message is stripped in prod?
478 + value = new Error(
479 + (getComponentNameFromFiber(sourceFiber) || 'A React component') +
480 + ' suspended while rendering, but no fallback UI was specified.\n' +
481 + '\n' +
482 + 'Add a <Suspense fallback=...> component higher in the tree to ' +
483 + 'provide a loading indicator or placeholder to display.',
484 + );
485 + }
486 + } else {
487 + // This is a regular error, not a Suspense wakeable.
488 + if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
489 + // If the error was thrown during hydration, we may be able to recover by
490 + // discarding the dehydrated content and switching to a client render.
491 + // Instead of surfacing the error, find the nearest Suspense boundary
492 + // and render it again without hydration.
493 + const suspenseBoundary = markNearestSuspenseBoundaryShouldCapture(
494 + returnFiber,
495 + sourceFiber,
496 + root,
497 + rootRenderLanes,
498 + );
499 + if (suspenseBoundary !== null) {
500 + // Set a flag to indicate that we should try rendering the normal
501 + // children again, not the fallback.
502 + suspenseBoundary.flags |= ForceClientRender;
503 return;
504 }
416 - // This boundary already captured during this render. Continue to the next
417 - // boundary.
418 - workInProgress = workInProgress.return;
419 - } while (workInProgress !== null);
420 - // No boundary was found. Fallthrough to error mode.
421 - // TODO: Use invariant so the message is stripped in prod?
422 - value = new Error(
423 - (getComponentNameFromFiber(sourceFiber) || 'A React component') +
424 - ' suspended while rendering, but no fallback UI was specified.\n' +
425 - '\n' +
426 - 'Add a <Suspense fallback=...> component higher in the tree to ' +
427 - 'provide a loading indicator or placeholder to display.',
428 - );
505 + } else {
506 + // Otherwise, fall through to the error path.
507 + }
508 }
509
510 // We didn't find a boundary that could handle this type of exception. Start