@samitouri / QOS-React-2 / commits / 50addf4c0e

Refactor Partial Hydration (#16346)

* Move dehydrated to be child of regular SuspenseComponent We now store the comment node on SuspenseState instead and that indicates that this SuspenseComponent is still dehydrated. We also store a child but that is only used to represent the DOM node for deletions and getNextHostSibling. * Move logic from DehydratedSuspenseComponent to SuspenseComponent Forked based on SuspenseState.dehydrated instead. * Retry logic for dehydrated boundary We can now simplify the logic for retrying dehydrated boundaries without hydrating. This is becomes simply a reconciliation against the dehydrated fragment which gets deleted, and the new children gets inserted. * Remove dehydrated from throw Instead we use the regular Suspense path. To save code, we attach retry listeners in the commit phase even though technically we don't have to. * Pop to nearest Suspense I think this is right...? * Popping hydration state should skip past the dehydrated instance * Split mount from update and special case suspended second pass The DidCapture flag isn't used consistently in the same way. We need further refactor for this. * Reorganize update path If we remove the dehydration status in the first pass and then do a second pass because we suspended, then we need to continue as if it didn't previously suspend. Since there is no fragment child etc. However, we must readd the deletion. * Schedule context work on the boundary and not the child * Warn for Suspense hydration in legacy mode It does a two pass render that client renders the content. * Rename DehydratedSuspenseComponent -> DehydratedFragment This now doesn't represent a suspense boundary itself. Its parent does. This Fiber represents the fragment around the dehydrated content. * Refactor returns Avoids the temporary mutable variables. I kept losing track of them. * Add a comment explaining the type. Placing it in the type since that's the central point as opposed to spread out.

Sebastian Markbåge committed Aug 12, 2019 at 15:58 UTC 50addf4c0e411e351de7290c8c60ec775c25c8c4
15 files changed +453 -292
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+75 -2
@@ -90,6 +90,77 @@ describe('ReactDOMServerPartialHydration', () => {
90 expect(ref.current).toBe(span);
91 });
92
93 + it('warns and replaces the boundary content in legacy mode', async () => {
94 + let suspend = false;
95 + let resolve;
96 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
97 + let ref = React.createRef();
98 +
99 + function Child() {
100 + if (suspend) {
101 + throw promise;
102 + } else {
103 + return 'Hello';
104 + }
105 + }
106 +
107 + function App() {
108 + return (
109 + <div>
110 + <Suspense fallback="Loading...">
111 + <span ref={ref}>
112 + <Child />
113 + </span>
114 + </Suspense>
115 + </div>
116 + );
117 + }
118 +
119 + // Don't suspend on the server.
120 + suspend = false;
121 + let finalHTML = ReactDOMServer.renderToString(<App />);
122 +
123 + let container = document.createElement('div');
124 + container.innerHTML = finalHTML;
125 +
126 + let span = container.getElementsByTagName('span')[0];
127 +
128 + // On the client we try to hydrate.
129 + suspend = true;
130 + expect(() => {
131 + act(() => {
132 + ReactDOM.hydrate(<App />, container);
133 + });
134 + }).toWarnDev(
135 + 'Warning: Cannot hydrate Suspense in legacy mode. Switch from ' +
136 + 'ReactDOM.hydrate(element, container) to ' +
137 + 'ReactDOM.unstable_createSyncRoot(container, { hydrate: true })' +
138 + '.render(element) or remove the Suspense components from the server ' +
139 + 'rendered components.' +
140 + '\n in Suspense (at **)' +
141 + '\n in div (at **)' +
142 + '\n in App (at **)',
143 + );
144 +
145 + // We're now in loading state.
146 + expect(container.textContent).toBe('Loading...');
147 +
148 + let span2 = container.getElementsByTagName('span')[0];
149 + // This is a new node.
150 + expect(span).not.toBe(span2);
151 + expect(ref.current).toBe(span2);
152 +
153 + // Resolving the promise should render the final content.
154 + suspend = false;
155 + resolve();
156 + await promise;
157 + Scheduler.unstable_flushAll();
158 + jest.runAllTimers();
159 +
160 + // We should now have hydrated with a ref on the existing span.
161 + expect(container.textContent).toBe('Hello');
162 + });
163 +
164 it('can insert siblings before the dehydrated boundary', () => {
165 let suspend = false;
166 let promise = new Promise(() => {});
@@ -135,7 +206,8 @@ describe('ReactDOMServerPartialHydration', () => {
206 suspend = true;
207
208 act(() => {
138 - ReactDOM.hydrate(<App />, container);
209 + let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
210 + root.render(<App />);
211 });
212
213 expect(container.firstChild.firstChild.tagName).not.toBe('DIV');
@@ -191,7 +263,8 @@ describe('ReactDOMServerPartialHydration', () => {
263 // hydrating anyway.
264 suspend = true;
265 act(() => {
194 - ReactDOM.hydrate(<App />, container);
266 + let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
267 + root.render(<App />);
268 });
269
270 expect(container.firstChild.children[1].textContent).toBe('Middle');
packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+16 -4
@@ -37,7 +37,7 @@ function initModules() {
37 };
38 }
39
40 -const {resetModules, serverRender, itRenders} = ReactDOMServerIntegrationUtils(
40 +const {resetModules, serverRender} = ReactDOMServerIntegrationUtils(
41 initModules,
42 );
43
@@ -102,8 +102,8 @@ describe('ReactDOMServerSuspense', () => {
102 );
103 });
104
105 - itRenders('a SuspenseList component and its children', async render => {
106 - const element = await render(
105 + it('server renders a SuspenseList component and its children', async () => {
106 + const example = (
107 <React.unstable_SuspenseList>
108 <React.Suspense fallback="Loading A">
109 <div>A</div>
@@ -111,8 +111,9 @@ describe('ReactDOMServerSuspense', () => {
111 <React.Suspense fallback="Loading B">
112 <div>B</div>
113 </React.Suspense>
114 - </React.unstable_SuspenseList>,
114 + </React.unstable_SuspenseList>
115 );
116 + const element = await serverRender(example);
117 const parent = element.parentNode;
118 const divA = parent.children[0];
119 expect(divA.tagName).toBe('DIV');
@@ -120,5 +121,16 @@ describe('ReactDOMServerSuspense', () => {
121 const divB = parent.children[1];
122 expect(divB.tagName).toBe('DIV');
123 expect(divB.textContent).toBe('B');
124 +
125 + ReactTestUtils.act(() => {
126 + const root = ReactDOM.unstable_createSyncRoot(parent, {hydrate: true});
127 + root.render(example);
128 + });
129 +
130 + const parent2 = element.parentNode;
131 + const divA2 = parent2.children[0];
132 + const divB2 = parent2.children[1];
133 + expect(divA).toBe(divA2);
134 + expect(divB).toBe(divB2);
135 });
136 });
packages/react-reconciler/src/ReactDebugFiberPerf.js
+1 -3
@@ -21,7 +21,6 @@ import {
21 ContextConsumer,
22 Mode,
23 SuspenseComponent,
24 - DehydratedSuspenseComponent,
24 } from 'shared/ReactWorkTags';
25
26 type MeasurementPhase =
@@ -317,8 +316,7 @@ export function stopFailedWorkTimer(fiber: Fiber): void {
316 }
317 fiber._debugIsCurrentlyTiming = false;
318 const warning =
320 - fiber.tag === SuspenseComponent ||
321 - fiber.tag === DehydratedSuspenseComponent
319 + fiber.tag === SuspenseComponent
320 ? 'Rendering was suspended'
321 : 'An error was thrown inside this error boundary';
322 endFiberMark(fiber, null, warning);
packages/react-reconciler/src/ReactFiber.js
+10
@@ -24,6 +24,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
24 import type {UpdateQueue} from './ReactUpdateQueue';
25 import type {ContextDependency} from './ReactFiberNewContext';
26 import type {HookType} from './ReactFiberHooks';
27 +import type {SuspenseInstance} from './ReactFiberHostConfig';
28
29 import invariant from 'shared/invariant';
30 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -48,6 +49,7 @@ import {
49 Profiler,
50 SuspenseComponent,
51 SuspenseListComponent,
52 + DehydratedFragment,
53 FunctionComponent,
54 MemoComponent,
55 SimpleMemoComponent,
@@ -843,6 +845,14 @@ export function createFiberFromHostInstanceForDeletion(): Fiber {
845 return fiber;
846 }
847
848 +export function createFiberFromDehydratedFragment(
849 + dehydratedNode: SuspenseInstance,
850 +): Fiber {
851 + const fiber = createFiber(DehydratedFragment, null, null, NoMode);
852 + fiber.stateNode = dehydratedNode;
853 + return fiber;
854 +}
855 +
856 export function createFiberFromPortal(
857 portal: ReactPortal,
858 mode: TypeOfMode,
packages/react-reconciler/src/ReactFiberBeginWork.js
+228 -148
@@ -36,7 +36,6 @@ import {
36 Profiler,
37 SuspenseComponent,
38 SuspenseListComponent,
39 - DehydratedSuspenseComponent,
39 MemoComponent,
40 SimpleMemoComponent,
41 LazyComponent,
@@ -93,6 +92,7 @@ import {processUpdateQueue} from './ReactUpdateQueue';
92 import {
93 NoWork,
94 Never,
95 + Sync,
96 computeAsyncExpiration,
97 } from './ReactFiberExpirationTime';
98 import {
@@ -115,7 +115,6 @@ import {pushHostContext, pushHostContainer} from './ReactFiberHostContext';
115 import {
116 suspenseStackCursor,
117 pushSuspenseContext,
118 - popSuspenseContext,
118 InvisibleParentSuspenseContext,
119 ForceSuspenseFallback,
120 hasSuspenseContext,
@@ -1475,8 +1474,9 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
1474 }
1475 }
1476
1478 -// TODO: This is now an empty object. Should we just make it a boolean?
1479 -const SUSPENDED_MARKER: SuspenseState = ({}: any);
1477 +const SUSPENDED_MARKER: SuspenseState = {
1478 + dehydrated: null,
1479 +};
1480
1481 function shouldRemainOnFallback(
1482 suspenseContext: SuspenseContext,
@@ -1511,21 +1511,23 @@ function updateSuspenseComponent(
1511
1512 let suspenseContext: SuspenseContext = suspenseStackCursor.current;
1513
1514 - let nextState = null;
1514 let nextDidTimeout = false;
1515 + const didSuspend = (workInProgress.effectTag & DidCapture) !== NoEffect;
1516
1517 if (
1518 - (workInProgress.effectTag & DidCapture) !== NoEffect ||
1518 + didSuspend ||
1519 shouldRemainOnFallback(suspenseContext, current, workInProgress)
1520 ) {
1521 // Something in this boundary's subtree already suspended. Switch to
1522 // rendering the fallback children.
1523 - nextState = SUSPENDED_MARKER;
1523 nextDidTimeout = true;
1524 workInProgress.effectTag &= ~DidCapture;
1525 } else {
1526 // Attempting the main content
1528 - if (current === null || current.memoizedState !== null) {
1527 + if (
1528 + current === null ||
1529 + (current.memoizedState: null | SuspenseState) !== null
1530 + ) {
1531 // This is a new mount or this boundary is already showing a fallback state.
1532 // Mark this subtree context as having at least one invisible parent that could
1533 // handle the fallback state.
@@ -1582,29 +1584,24 @@ function updateSuspenseComponent(
1584 // custom reconciliation logic to preserve the state of the primary
1585 // children. It's essentially a very basic form of re-parenting.
1586
1585 - // `child` points to the child fiber. In the normal case, this is the first
1586 - // fiber of the primary children set. In the timed-out case, it's a
1587 - // a fragment fiber containing the primary children.
1588 - let child;
1589 - // `next` points to the next fiber React should render. In the normal case,
1590 - // it's the same as `child`: the first fiber of the primary children set.
1591 - // In the timed-out case, it's a fragment fiber containing the *fallback*
1592 - // children -- we skip over the primary children entirely.
1593 - let next;
1587 if (current === null) {
1588 if (enableSuspenseServerRenderer) {
1589 // If we're currently hydrating, try to hydrate this boundary.
1590 // But only if this has a fallback.
1591 if (nextProps.fallback !== undefined) {
1592 tryToClaimNextHydratableInstance(workInProgress);
1600 - // This could've changed the tag if this was a dehydrated suspense component.
1601 - if (workInProgress.tag === DehydratedSuspenseComponent) {
1602 - popSuspenseContext(workInProgress);
1603 - return updateDehydratedSuspenseComponent(
1604 - null,
1605 - workInProgress,
1606 - renderExpirationTime,
1607 - );
1593 + // This could've been a dehydrated suspense component.
1594 + const suspenseState: null | SuspenseState =
1595 + workInProgress.memoizedState;
1596 + if (suspenseState !== null) {
1597 + const dehydrated = suspenseState.dehydrated;
1598 + if (dehydrated !== null) {
1599 + return mountDehydratedSuspenseComponent(
1600 + workInProgress,
1601 + dehydrated,
1602 + renderExpirationTime,
1603 + );
1604 + }
1605 }
1606 }
1607 }
@@ -1646,26 +1643,121 @@ function updateSuspenseComponent(
1643 );
1644 fallbackChildFragment.return = workInProgress;
1645 primaryChildFragment.sibling = fallbackChildFragment;
1649 - child = primaryChildFragment;
1646 // Skip the primary children, and continue working on the
1647 // fallback children.
1652 - next = fallbackChildFragment;
1648 + workInProgress.memoizedState = SUSPENDED_MARKER;
1649 + workInProgress.child = primaryChildFragment;
1650 + return fallbackChildFragment;
1651 } else {
1652 // Mount the primary children without an intermediate fragment fiber.
1653 const nextPrimaryChildren = nextProps.children;
1656 - child = next = mountChildFibers(
1654 + workInProgress.memoizedState = null;
1655 + return (workInProgress.child = mountChildFibers(
1656 workInProgress,
1657 null,
1658 nextPrimaryChildren,
1659 renderExpirationTime,
1661 - );
1660 + ));
1661 }
1662 } else {
1663 // This is an update. This branch is more complicated because we need to
1664 // ensure the state of the primary children is preserved.
1666 - const prevState = current.memoizedState;
1667 - const prevDidTimeout = prevState !== null;
1668 - if (prevDidTimeout) {
1665 + const prevState: null | SuspenseState = current.memoizedState;
1666 + if (prevState !== null) {
1667 + if (enableSuspenseServerRenderer) {
1668 + const dehydrated = prevState.dehydrated;
1669 + if (dehydrated !== null) {
1670 + if (!didSuspend) {
1671 + return updateDehydratedSuspenseComponent(
1672 + current,
1673 + workInProgress,
1674 + dehydrated,
1675 + renderExpirationTime,
1676 + );
1677 + } else if (
1678 + (workInProgress.memoizedState: null | SuspenseState) !== null
1679 + ) {
1680 + // Something suspended and we should still be in dehydrated mode.
1681 + // Leave the existing child in place.
1682 + workInProgress.child = current.child;
1683 + // The dehydrated completion pass expects this flag to be there
1684 + // but the normal suspense pass doesn't.
1685 + workInProgress.effectTag |= DidCapture;
1686 + return null;
1687 + } else {
1688 + // Suspended but we should no longer be in dehydrated mode.
1689 + // Therefore we now have to render the fallback. Wrap the children
1690 + // in a fragment fiber to keep them separate from the fallback
1691 + // children.
1692 + const nextFallbackChildren = nextProps.fallback;
1693 + const primaryChildFragment = createFiberFromFragment(
1694 + // It shouldn't matter what the pending props are because we aren't
1695 + // going to render this fragment.
1696 + null,
1697 + mode,
1698 + NoWork,
1699 + null,
1700 + );
1701 + primaryChildFragment.return = workInProgress;
1702 +
1703 + // This is always null since we never want the previous child
1704 + // that we're not going to hydrate.
1705 + primaryChildFragment.child = null;
1706 +
1707 + if ((workInProgress.mode & BatchedMode) === NoMode) {
1708 + // Outside of batched mode, we commit the effects from the
1709 + // partially completed, timed-out tree, too.
1710 + let progressedChild = (primaryChildFragment.child =
1711 + workInProgress.child);
1712 + while (progressedChild !== null) {
1713 + progressedChild.return = primaryChildFragment;
1714 + progressedChild = progressedChild.sibling;
1715 + }
1716 + } else {
1717 + // We will have dropped the effect list which contains the deletion.
1718 + // We need to reconcile to delete the current child.
1719 + reconcileChildFibers(
1720 + workInProgress,
1721 + current.child,
1722 + null,
1723 + renderExpirationTime,
1724 + );
1725 + }
1726 +
1727 + // Because primaryChildFragment is a new fiber that we're inserting as the
1728 + // parent of a new tree, we need to set its treeBaseDuration.
1729 + if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1730 + // treeBaseDuration is the sum of all the child tree base durations.
1731 + let treeBaseDuration = 0;
1732 + let hiddenChild = primaryChildFragment.child;
1733 + while (hiddenChild !== null) {
1734 + treeBaseDuration += hiddenChild.treeBaseDuration;
1735 + hiddenChild = hiddenChild.sibling;
1736 + }
1737 + primaryChildFragment.treeBaseDuration = treeBaseDuration;
1738 + }
1739 +
1740 + // Create a fragment from the fallback children, too.
1741 + const fallbackChildFragment = createFiberFromFragment(
1742 + nextFallbackChildren,
1743 + mode,
1744 + renderExpirationTime,
1745 + null,
1746 + );
1747 + fallbackChildFragment.return = workInProgress;
1748 + primaryChildFragment.sibling = fallbackChildFragment;
1749 + fallbackChildFragment.effectTag |= Placement;
1750 + primaryChildFragment.childExpirationTime = NoWork;
1751 +
1752 + workInProgress.memoizedState = SUSPENDED_MARKER;
1753 + workInProgress.child = primaryChildFragment;
1754 +
1755 + // Skip the primary children, and continue working on the
1756 + // fallback children.
1757 + return fallbackChildFragment;
1758 + }
1759 + }
1760 + }
1761 // The current tree already timed out. That means each child set is
1762 // wrapped in a fragment fiber.
1763 const currentPrimaryChildFragment: Fiber = (current.child: any);
@@ -1721,11 +1813,12 @@ function updateSuspenseComponent(
1813 );
1814 fallbackChildFragment.return = workInProgress;
1815 primaryChildFragment.sibling = fallbackChildFragment;
1724 - child = primaryChildFragment;
1816 primaryChildFragment.childExpirationTime = NoWork;
1817 // Skip the primary children, and continue working on the
1818 // fallback children.
1728 - next = fallbackChildFragment;
1819 + workInProgress.memoizedState = SUSPENDED_MARKER;
1820 + workInProgress.child = primaryChildFragment;
1821 + return fallbackChildFragment;
1822 } else {
1823 // No longer suspended. Switch back to showing the primary children,
1824 // and remove the intermediate fragment fiber.
@@ -1745,7 +1838,8 @@ function updateSuspenseComponent(
1838 // the stateNode?
1839
1840 // Continue rendering the children, like we normally do.
1748 - child = next = primaryChild;
1841 + workInProgress.memoizedState = null;
1842 + return (workInProgress.child = primaryChild);
1843 }
1844 } else {
1845 // The current tree has not already timed out. That means the primary
@@ -1813,29 +1907,26 @@ function updateSuspenseComponent(
1907 fallbackChildFragment.return = workInProgress;
1908 primaryChildFragment.sibling = fallbackChildFragment;
1909 fallbackChildFragment.effectTag |= Placement;
1816 - child = primaryChildFragment;
1910 primaryChildFragment.childExpirationTime = NoWork;
1911 // Skip the primary children, and continue working on the
1912 // fallback children.
1820 - next = fallbackChildFragment;
1913 + workInProgress.memoizedState = SUSPENDED_MARKER;
1914 + workInProgress.child = primaryChildFragment;
1915 + return fallbackChildFragment;
1916 } else {
1917 // Still haven't timed out. Continue rendering the children, like we
1918 // normally do.
1919 + workInProgress.memoizedState = null;
1920 const nextPrimaryChildren = nextProps.children;
1825 - next = child = reconcileChildFibers(
1921 + return (workInProgress.child = reconcileChildFibers(
1922 workInProgress,
1923 currentPrimaryChild,
1924 nextPrimaryChildren,
1925 renderExpirationTime,
1830 - );
1926 + ));
1927 }
1928 }
1833 - workInProgress.stateNode = current.stateNode;
1929 }
1835 -
1836 - workInProgress.memoizedState = nextState;
1837 - workInProgress.child = child;
1838 - return next;
1930 }
1931
1932 function retrySuspenseComponentWithoutHydrating(
@@ -1843,94 +1934,90 @@ function retrySuspenseComponentWithoutHydrating(
1934 workInProgress: Fiber,
1935 renderExpirationTime: ExpirationTime,
1936 ) {
1846 - // Detach from the current dehydrated boundary.
1847 - current.alternate = null;
1848 - workInProgress.alternate = null;
1849 -
1850 - // Insert a deletion in the effect list.
1851 - let returnFiber = workInProgress.return;
1852 - invariant(
1853 - returnFiber !== null,
1854 - 'Suspense boundaries are never on the root. ' +
1855 - 'This is probably a bug in React.',
1856 - );
1857 - const last = returnFiber.lastEffect;
1858 - if (last !== null) {
1859 - last.nextEffect = current;
1860 - returnFiber.lastEffect = current;
1861 - } else {
1862 - returnFiber.firstEffect = returnFiber.lastEffect = current;
1863 - }
1864 - current.nextEffect = null;
1865 - current.effectTag = Deletion;
1866 -
1867 - popSuspenseContext(workInProgress);
1868 -
1869 - // Upgrade this work in progress to a real Suspense component.
1870 - workInProgress.tag = SuspenseComponent;
1871 - workInProgress.stateNode = null;
1937 + // We're now not suspended nor dehydrated.
1938 workInProgress.memoizedState = null;
1873 - // This is now an insertion.
1874 - workInProgress.effectTag |= Placement;
1875 - // Retry as a real Suspense component.
1876 - return updateSuspenseComponent(null, workInProgress, renderExpirationTime);
1939 + // Retry with the full children.
1940 + const nextProps = workInProgress.pendingProps;
1941 + const nextChildren = nextProps.children;
1942 + // This will ensure that the children get Placement effects and
1943 + // that the old child gets a Deletion effect.
1944 + // We could also call forceUnmountCurrentAndReconcile.
1945 + reconcileChildren(
1946 + current,
1947 + workInProgress,
1948 + nextChildren,
1949 + renderExpirationTime,
1950 + );
1951 + return workInProgress.child;
1952 }
1953
1879 -function updateDehydratedSuspenseComponent(
1880 - current: Fiber | null,
1954 +function mountDehydratedSuspenseComponent(
1955 workInProgress: Fiber,
1956 + suspenseInstance: SuspenseInstance,
1957 renderExpirationTime: ExpirationTime,
1883 -) {
1884 - pushSuspenseContext(
1885 - workInProgress,
1886 - setDefaultShallowSuspenseContext(suspenseStackCursor.current),
1887 - );
1888 - const suspenseInstance = (workInProgress.stateNode: SuspenseInstance);
1889 - if (current === null) {
1890 - // During the first pass, we'll bail out and not drill into the children.
1891 - // Instead, we'll leave the content in place and try to hydrate it later.
1892 - if (isSuspenseInstanceFallback(suspenseInstance)) {
1893 - // This is a client-only boundary. Since we won't get any content from the server
1894 - // for this, we need to schedule that at a higher priority based on when it would
1895 - // have timed out. In theory we could render it in this pass but it would have the
1896 - // wrong priority associated with it and will prevent hydration of parent path.
1897 - // Instead, we'll leave work left on it to render it in a separate commit.
1898 -
1899 - // TODO This time should be the time at which the server rendered response that is
1900 - // a parent to this boundary was displayed. However, since we currently don't have
1901 - // a protocol to transfer that time, we'll just estimate it by using the current
1902 - // time. This will mean that Suspense timeouts are slightly shifted to later than
1903 - // they should be.
1904 - let serverDisplayTime = requestCurrentTime();
1905 - // Schedule a normal pri update to render this content.
1906 - let newExpirationTime = computeAsyncExpiration(serverDisplayTime);
1907 - if (enableSchedulerTracing) {
1908 - markSpawnedWork(newExpirationTime);
1909 - }
1910 - workInProgress.expirationTime = newExpirationTime;
1911 - } else {
1912 - // We'll continue hydrating the rest at offscreen priority since we'll already
1913 - // be showing the right content coming from the server, it is no rush.
1914 - workInProgress.expirationTime = Never;
1915 - if (enableSchedulerTracing) {
1916 - markSpawnedWork(Never);
1917 - }
1958 +): null | Fiber {
1959 + // During the first pass, we'll bail out and not drill into the children.
1960 + // Instead, we'll leave the content in place and try to hydrate it later.
1961 + if ((workInProgress.mode & BatchedMode) === NoMode) {
1962 + if (__DEV__) {
1963 + warning(
1964 + false,
1965 + 'Cannot hydrate Suspense in legacy mode. Switch from ' +
1966 + 'ReactDOM.hydrate(element, container) to ' +
1967 + 'ReactDOM.unstable_createSyncRoot(container, { hydrate: true })' +
1968 + '.render(element) or remove the Suspense components from ' +
1969 + 'the server rendered components.',
1970 + );
1971 + }
1972 + workInProgress.expirationTime = Sync;
1973 + } else if (isSuspenseInstanceFallback(suspenseInstance)) {
1974 + // This is a client-only boundary. Since we won't get any content from the server
1975 + // for this, we need to schedule that at a higher priority based on when it would
1976 + // have timed out. In theory we could render it in this pass but it would have the
1977 + // wrong priority associated with it and will prevent hydration of parent path.
1978 + // Instead, we'll leave work left on it to render it in a separate commit.
1979 +
1980 + // TODO This time should be the time at which the server rendered response that is
1981 + // a parent to this boundary was displayed. However, since we currently don't have
1982 + // a protocol to transfer that time, we'll just estimate it by using the current
1983 + // time. This will mean that Suspense timeouts are slightly shifted to later than
1984 + // they should be.
1985 + let serverDisplayTime = requestCurrentTime();
1986 + // Schedule a normal pri update to render this content.
1987 + let newExpirationTime = computeAsyncExpiration(serverDisplayTime);
1988 + if (enableSchedulerTracing) {
1989 + markSpawnedWork(newExpirationTime);
1990 + }
1991 + workInProgress.expirationTime = newExpirationTime;
1992 + } else {
1993 + // We'll continue hydrating the rest at offscreen priority since we'll already
1994 + // be showing the right content coming from the server, it is no rush.
1995 + workInProgress.expirationTime = Never;
1996 + if (enableSchedulerTracing) {
1997 + markSpawnedWork(Never);
1998 }
1919 -
1920 - return null;
1921 - }
1922 -
1923 - if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
1924 - // Something suspended. Leave the existing children in place.
1925 - // TODO: In non-concurrent mode, should we commit the nodes we have hydrated so far?
1926 - workInProgress.child = null;
1927 - return null;
1999 }
2000 + return null;
2001 +}
2002
2003 +function updateDehydratedSuspenseComponent(
2004 + current: Fiber,
2005 + workInProgress: Fiber,
2006 + suspenseInstance: SuspenseInstance,
2007 + renderExpirationTime: ExpirationTime,
2008 +): null | Fiber {
2009 // We should never be hydrating at this point because it is the first pass,
2010 // but after we've already committed once.
2011 warnIfHydrating();
2012
2013 + if ((workInProgress.mode & BatchedMode) === NoMode) {
2014 + return retrySuspenseComponentWithoutHydrating(
2015 + current,
2016 + workInProgress,
2017 + renderExpirationTime,
2018 + );
2019 + }
2020 +
2021 if (isSuspenseInstanceFallback(suspenseInstance)) {
2022 // This boundary is in a permanent fallback state. In this case, we'll never
2023 // get an update and we'll never be able to hydrate the final content. Let's just try the
@@ -1967,8 +2054,8 @@ function updateDehydratedSuspenseComponent(
2054 // these should update this boundary to the permanent Fallback state instead.
2055 // Mark it as having captured (i.e. suspended).
2056 workInProgress.effectTag |= DidCapture;
1970 - // Leave the children in place. I.e. empty.
1971 - workInProgress.child = null;
2057 + // Leave the child in place. I.e. the dehydrated fragment.
2058 + workInProgress.child = current.child;
2059 // Register a callback to retry this boundary once the server has sent the result.
2060 registerSuspenseInstanceRetry(
2061 suspenseInstance,
@@ -1977,7 +2064,10 @@ function updateDehydratedSuspenseComponent(
2064 return null;
2065 } else {
2066 // This is the first attempt.
1980 - reenterHydrationStateFromDehydratedSuspenseInstance(workInProgress);
2067 + reenterHydrationStateFromDehydratedSuspenseInstance(
2068 + workInProgress,
2069 + suspenseInstance,
2070 + );
2071 const nextProps = workInProgress.pendingProps;
2072 const nextChildren = nextProps.children;
2073 workInProgress.child = mountChildFibers(
@@ -2732,8 +2822,21 @@ function beginWork(
2822 break;
2823 case SuspenseComponent: {
2824 const state: SuspenseState | null = workInProgress.memoizedState;
2735 - const didTimeout = state !== null;
2736 - if (didTimeout) {
2825 + if (state !== null) {
2826 + if (enableSuspenseServerRenderer) {
2827 + if (state.dehydrated !== null) {
2828 + pushSuspenseContext(
2829 + workInProgress,
2830 + setDefaultShallowSuspenseContext(suspenseStackCursor.current),
2831 + );
2832 + // We know that this component will suspend again because if it has
2833 + // been unsuspended it has committed as a resolved Suspense component.
2834 + // If it needs to be retried, it should have work scheduled on it.
2835 + workInProgress.effectTag |= DidCapture;
2836 + break;
2837 + }
2838 + }
2839 +
2840 // If this boundary is currently timed out, we need to decide
2841 // whether to retry the primary children, or to skip over it and
2842 // go straight to the fallback. Check the priority of the primary
@@ -2780,19 +2883,6 @@ function beginWork(
2883 }
2884 break;
2885 }
2783 - case DehydratedSuspenseComponent: {
2784 - if (enableSuspenseServerRenderer) {
2785 - pushSuspenseContext(
2786 - workInProgress,
2787 - setDefaultShallowSuspenseContext(suspenseStackCursor.current),
2788 - );
2789 - // We know that this component will suspend again because if it has
2790 - // been unsuspended it has committed as a regular Suspense component.
2791 - // If it needs to be retried, it should have work scheduled on it.
2792 - workInProgress.effectTag |= DidCapture;
2793 - }
2794 - break;
2795 - }
2886 case SuspenseListComponent: {
2887 const didSuspendBefore =
2888 (current.effectTag & DidCapture) !== NoEffect;
@@ -3010,16 +3100,6 @@ function beginWork(
3100 renderExpirationTime,
3101 );
3102 }
3013 - case DehydratedSuspenseComponent: {
3014 - if (enableSuspenseServerRenderer) {
3015 - return updateDehydratedSuspenseComponent(
3016 - current,
3017 - workInProgress,
3018 - renderExpirationTime,
3019 - );
3020 - }
3021 - break;
3022 - }
3103 case SuspenseListComponent: {
3104 return updateSuspenseListComponent(
3105 current,
packages/react-reconciler/src/ReactFiberCommitWork.js
+3 -3
@@ -43,7 +43,7 @@ import {
43 HostPortal,
44 Profiler,
45 SuspenseComponent,
46 - DehydratedSuspenseComponent,
46 + DehydratedFragment,
47 IncompleteClassComponent,
48 MemoComponent,
49 SimpleMemoComponent,
@@ -967,7 +967,7 @@ function getHostSibling(fiber: Fiber): ?Instance {
967 while (
968 node.tag !== HostComponent &&
969 node.tag !== HostText &&
970 - node.tag !== DehydratedSuspenseComponent
970 + node.tag !== DehydratedFragment
971 ) {
972 // If it is not host node and, we might have a host node inside it.
973 // Try to search down until we find one.
@@ -1162,7 +1162,7 @@ function unmountHostComponents(current, renderPriorityLevel): void {
1162 }
1163 } else if (
1164 enableSuspenseServerRenderer &&
1165 - node.tag === DehydratedSuspenseComponent
1165 + node.tag === DehydratedFragment
1166 ) {
1167 // Delete the dehydrated suspense boundary and all of its content.
1168 if (currentParentIsContainer) {
packages/react-reconciler/src/ReactFiberCompleteWork.js
+34 -35
@@ -48,7 +48,6 @@ import {
48 Profiler,
49 SuspenseComponent,
50 SuspenseListComponent,
51 - DehydratedSuspenseComponent,
51 MemoComponent,
52 SimpleMemoComponent,
53 LazyComponent,
@@ -115,7 +114,6 @@ import {popProvider} from './ReactFiberNewContext';
114 import {
115 prepareToHydrateHostInstance,
116 prepareToHydrateHostTextInstance,
118 - skipPastDehydratedSuspenseInstance,
117 popHydrationState,
118 resetHydrationState,
119 } from './ReactFiberHydrationContext';
@@ -838,6 +836,38 @@ function completeWork(
836 case SuspenseComponent: {
837 popSuspenseContext(workInProgress);
838 const nextState: null | SuspenseState = workInProgress.memoizedState;
839 +
840 + if (enableSuspenseServerRenderer) {
841 + if (nextState !== null && nextState.dehydrated !== null) {
842 + if (current === null) {
843 + let wasHydrated = popHydrationState(workInProgress);
844 + invariant(
845 + wasHydrated,
846 + 'A dehydrated suspense component was completed without a hydrated node. ' +
847 + 'This is probably a bug in React.',
848 + );
849 + if (enableSchedulerTracing) {
850 + markSpawnedWork(Never);
851 + }
852 + return null;
853 + } else {
854 + // We should never have been in a hydration state if we didn't have a current.
855 + // However, in some of those paths, we might have reentered a hydration state
856 + // and then we might be inside a hydration state. In that case, we'll need to
857 + // exit out of it.
858 + resetHydrationState();
859 + if ((workInProgress.effectTag & DidCapture) === NoEffect) {
860 + // This boundary did not suspend so it's now hydrated and unsuspended.
861 + workInProgress.memoizedState = null;
862 + } else {
863 + // Something suspended. Schedule an effect to attach retry listeners.
864 + workInProgress.effectTag |= Update;
865 + }
866 + return null;
867 + }
868 + }
869 + }
870 +
871 if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
872 // Something suspended. Re-render with the fallback children.
873 workInProgress.expirationTime = renderExpirationTime;
@@ -849,8 +879,8 @@ function completeWork(
879 let prevDidTimeout = false;
880 if (current === null) {
881 // In cases where we didn't find a suitable hydration boundary we never
852 - // downgraded this to a DehydratedSuspenseComponent, but we still need to
853 - // pop the hydration state since we might be inside the insertion tree.
882 + // put this in dehydrated mode, but we still need to pop the hydration
883 + // state since we might be inside the insertion tree.
884 popHydrationState(workInProgress);
885 } else {
886 const prevState: null | SuspenseState = current.memoizedState;
@@ -969,37 +999,6 @@ function completeWork(
999 }
1000 break;
1001 }
972 - case DehydratedSuspenseComponent: {
973 - if (enableSuspenseServerRenderer) {
974 - popSuspenseContext(workInProgress);
975 - if (current === null) {
976 - let wasHydrated = popHydrationState(workInProgress);
977 - invariant(
978 - wasHydrated,
979 - 'A dehydrated suspense component was completed without a hydrated node. ' +
980 - 'This is probably a bug in React.',
981 - );
982 - skipPastDehydratedSuspenseInstance(workInProgress);
983 - } else {
984 - // We should never have been in a hydration state if we didn't have a current.
985 - // However, in some of those paths, we might have reentered a hydration state
986 - // and then we might be inside a hydration state. In that case, we'll need to
987 - // exit out of it.
988 - resetHydrationState();
989 - if ((workInProgress.effectTag & DidCapture) === NoEffect) {
990 - // This boundary did not suspend so it's now hydrated.
991 - // To handle any future suspense cases, we're going to now upgrade it
992 - // to a Suspense component. We detach it from the existing current fiber.
993 - current.alternate = null;
994 - workInProgress.alternate = null;
995 - workInProgress.tag = SuspenseComponent;
996 - workInProgress.memoizedState = null;
997 - workInProgress.stateNode = null;
998 - }
999 - }
1000 - }
1001 - break;
1002 - }
1002 case SuspenseListComponent: {
1003 popSuspenseContext(workInProgress);
1004
packages/react-reconciler/src/ReactFiberHydrationContext.js
+37 -18
@@ -16,18 +16,21 @@ import type {
16 Container,
17 HostContext,
18 } from './ReactFiberHostConfig';
19 +import type {SuspenseState} from './ReactFiberSuspenseComponent';
20
21 import {
22 HostComponent,
23 HostText,
24 HostRoot,
25 SuspenseComponent,
25 - DehydratedSuspenseComponent,
26 } from 'shared/ReactWorkTags';
27 import {Deletion, Placement} from 'shared/ReactSideEffectTags';
28 import invariant from 'shared/invariant';
29
30 -import {createFiberFromHostInstanceForDeletion} from './ReactFiber';
30 +import {
31 + createFiberFromHostInstanceForDeletion,
32 + createFiberFromDehydratedFragment,
33 +} from './ReactFiber';
34 import {
35 shouldSetTextContent,
36 supportsHydration,
@@ -82,12 +85,11 @@ function enterHydrationState(fiber: Fiber): boolean {
85
86 function reenterHydrationStateFromDehydratedSuspenseInstance(
87 fiber: Fiber,
88 + suspenseInstance: SuspenseInstance,
89 ): boolean {
90 if (!supportsHydration) {
91 return false;
92 }
89 -
90 - const suspenseInstance = fiber.stateNode;
93 nextHydratableInstance = getNextHydratableSibling(suspenseInstance);
94 popToNextHostParent(fiber);
95 isHydrating = true;
@@ -221,11 +223,23 @@ function tryHydrate(fiber, nextInstance) {
223 }
224 case SuspenseComponent: {
225 if (enableSuspenseServerRenderer) {
224 - const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
226 + const suspenseInstance: null | SuspenseInstance = canHydrateSuspenseInstance(
227 + nextInstance,
228 + );
229 if (suspenseInstance !== null) {
226 - // Downgrade the tag to a dehydrated component until we've hydrated it.
227 - fiber.tag = DehydratedSuspenseComponent;
228 - fiber.stateNode = (suspenseInstance: SuspenseInstance);
230 + const suspenseState: SuspenseState = {
231 + dehydrated: suspenseInstance,
232 + };
233 + fiber.memoizedState = suspenseState;
234 + // Store the dehydrated fragment as a child fiber.
235 + // This simplifies the code for getHostSibling and deleting nodes,
236 + // since it doesn't have to consider all Suspense boundaries and
237 + // check if they're dehydrated ones or not.
238 + const dehydratedFragment = createFiberFromDehydratedFragment(
239 + suspenseInstance,
240 + );
241 + dehydratedFragment.return = fiber;
242 + fiber.child = dehydratedFragment;
243 return true;
244 }
245 }
@@ -354,7 +368,9 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
368 return shouldUpdate;
369 }
370
357 -function skipPastDehydratedSuspenseInstance(fiber: Fiber): void {
371 +function skipPastDehydratedSuspenseInstance(
372 + fiber: Fiber,
373 +): null | HydratableInstance {
374 if (!supportsHydration) {
375 invariant(
376 false,
@@ -362,15 +378,15 @@ function skipPastDehydratedSuspenseInstance(fiber: Fiber): void {
378 'This error is likely caused by a bug in React. Please file an issue.',
379 );
380 }
365 - let suspenseInstance = fiber.stateNode;
381 + let suspenseState: null | SuspenseState = fiber.memoizedState;
382 + let suspenseInstance: null | SuspenseInstance =
383 + suspenseState !== null ? suspenseState.dehydrated : null;
384 invariant(
385 suspenseInstance,
386 'Expected to have a hydrated suspense instance. ' +
387 'This error is likely caused by a bug in React. Please file an issue.',
388 );
371 - nextHydratableInstance = getNextHydratableInstanceAfterSuspenseInstance(
372 - suspenseInstance,
373 - );
389 + return getNextHydratableInstanceAfterSuspenseInstance(suspenseInstance);
390 }
391
392 function popToNextHostParent(fiber: Fiber): void {
@@ -379,7 +395,7 @@ function popToNextHostParent(fiber: Fiber): void {
395 parent !== null &&
396 parent.tag !== HostComponent &&
397 parent.tag !== HostRoot &&
382 - parent.tag !== DehydratedSuspenseComponent
398 + parent.tag !== SuspenseComponent
399 ) {
400 parent = parent.return;
401 }
@@ -425,9 +441,13 @@ function popHydrationState(fiber: Fiber): boolean {
441 }
442
443 popToNextHostParent(fiber);
428 - nextHydratableInstance = hydrationParentFiber
429 - ? getNextHydratableSibling(fiber.stateNode)
430 - : null;
444 + if (fiber.tag === SuspenseComponent) {
445 + nextHydratableInstance = skipPastDehydratedSuspenseInstance(fiber);
446 + } else {
447 + nextHydratableInstance = hydrationParentFiber
448 + ? getNextHydratableSibling(fiber.stateNode)
449 + : null;
450 + }
451 return true;
452 }
453
@@ -449,6 +469,5 @@ export {
469 tryToClaimNextHydratableInstance,
470 prepareToHydrateHostInstance,
471 prepareToHydrateHostTextInstance,
452 - skipPastDehydratedSuspenseInstance,
472 popHydrationState,
473 };
packages/react-reconciler/src/ReactFiberNewContext.js
+13 -8
@@ -25,7 +25,7 @@ import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
25 import {
26 ContextProvider,
27 ClassComponent,
28 - DehydratedSuspenseComponent,
28 + DehydratedFragment,
29 } from 'shared/ReactWorkTags';
30
31 import invariant from 'shared/invariant';
@@ -249,15 +249,20 @@ export function propagateContextChange(
249 nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
250 } else if (
251 enableSuspenseServerRenderer &&
252 - fiber.tag === DehydratedSuspenseComponent
252 + fiber.tag === DehydratedFragment
253 ) {
254 - // If a dehydrated suspense component is in this subtree, we don't know
254 + // If a dehydrated suspense bounudary is in this subtree, we don't know
255 // if it will have any context consumers in it. The best we can do is
256 - // mark it as having updates on its children.
257 - if (fiber.expirationTime < renderExpirationTime) {
258 - fiber.expirationTime = renderExpirationTime;
256 + // mark it as having updates.
257 + let parentSuspense = fiber.return;
258 + invariant(
259 + parentSuspense !== null,
260 + 'We just came from a parent so we must have had a parent. This is a bug in React.',
261 + );
262 + if (parentSuspense.expirationTime < renderExpirationTime) {
263 + parentSuspense.expirationTime = renderExpirationTime;
264 }
260 - let alternate = fiber.alternate;
265 + let alternate = parentSuspense.alternate;
266 if (
267 alternate !== null &&
268 alternate.expirationTime < renderExpirationTime
@@ -268,7 +273,7 @@ export function propagateContextChange(
273 // because we want to schedule this fiber as having work
274 // on its children. We'll use the childExpirationTime on
275 // this fiber to indicate that a context has changed.
271 - scheduleWorkOnParentPath(fiber, renderExpirationTime);
276 + scheduleWorkOnParentPath(parentSuspense, renderExpirationTime);
277 nextFiber = fiber.sibling;
278 } else {
279 // Traverse down.
packages/react-reconciler/src/ReactFiberSuspenseComponent.js
+18 -3
@@ -8,12 +8,23 @@
8 */
9
10 import type {Fiber} from './ReactFiber';
11 +import type {SuspenseInstance} from './ReactFiberHostConfig';
12 import {SuspenseComponent, SuspenseListComponent} from 'shared/ReactWorkTags';
13 import {NoEffect, DidCapture} from 'shared/ReactSideEffectTags';
14
14 -// TODO: This is now an empty object. Should we switch this to a boolean?
15 -// Alternatively we can make this use an effect tag similar to SuspenseList.
16 -export type SuspenseState = {||};
15 +// A null SuspenseState represents an unsuspended normal Suspense boundary.
16 +// A non-null SuspenseState means that it is blocked for one reason or another.
17 +// - A non-null dehydrated field means it's blocked pending hydration.
18 +// - A non-null dehydrated field can use isSuspenseInstancePending or
19 +// isSuspenseInstanceFallback to query the reason for being dehydrated.
20 +// - A null dehydrated field means it's blocked by something suspending and
21 +// we're currently showing a fallback instead.
22 +export type SuspenseState = {|
23 + // If this boundary is still dehydrated, we store the SuspenseInstance
24 + // here to indicate that it is dehydrated (flag) and for quick access
25 + // to check things like isSuspenseInstancePending.
26 + dehydrated: null | SuspenseInstance,
27 +|};
28
29 export type SuspenseListTailMode = 'collapsed' | 'hidden' | void;
30
@@ -42,6 +53,10 @@ export function shouldCaptureSuspense(
53 // fallback. Otherwise, don't capture and bubble to the next boundary.
54 const nextState: SuspenseState | null = workInProgress.memoizedState;
55 if (nextState !== null) {
56 + if (nextState.dehydrated !== null) {
57 + // A dehydrated boundary always captures.
58 + return true;
59 + }
60 return false;
61 }
62 const props = workInProgress.memoizedProps;
packages/react-reconciler/src/ReactFiberThrow.js
+1 -39
@@ -22,7 +22,6 @@ import {
22 ClassComponent,
23 HostRoot,
24 SuspenseComponent,
25 - DehydratedSuspenseComponent,
25 IncompleteClassComponent,
26 } from 'shared/ReactWorkTags';
27 import {
@@ -32,10 +31,7 @@ import {
31 ShouldCapture,
32 LifecycleEffectMask,
33 } from 'shared/ReactSideEffectTags';
35 -import {
36 - enableSchedulerTracing,
37 - enableSuspenseServerRenderer,
38 -} from 'shared/ReactFeatureFlags';
34 +import {enableSchedulerTracing} from 'shared/ReactFeatureFlags';
35 import {NoMode, BatchedMode} from './ReactTypeOfMode';
36 import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent';
37
@@ -61,15 +57,11 @@ import {
57 markLegacyErrorBoundaryAsFailed,
58 isAlreadyFailedLegacyErrorBoundary,
59 pingSuspendedRoot,
64 - resolveRetryThenable,
60 checkForWrongSuspensePriorityInDEV,
61 } from './ReactFiberWorkLoop';
62
68 -import invariant from 'shared/invariant';
69 -
63 import {Sync} from './ReactFiberExpirationTime';
64
72 -const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
65 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
66
67 function createRootErrorUpdate(
@@ -323,36 +315,6 @@ function throwException(
315 workInProgress.effectTag |= ShouldCapture;
316 workInProgress.expirationTime = renderExpirationTime;
317
326 - return;
327 - } else if (
328 - enableSuspenseServerRenderer &&
329 - workInProgress.tag === DehydratedSuspenseComponent
330 - ) {
331 - attachPingListener(root, renderExpirationTime, thenable);
332 -
333 - // Since we already have a current fiber, we can eagerly add a retry listener.
334 - let retryCache = workInProgress.memoizedState;
335 - if (retryCache === null) {
336 - retryCache = workInProgress.memoizedState = new PossiblyWeakSet();
337 - const current = workInProgress.alternate;
338 - invariant(
339 - current,
340 - 'A dehydrated suspense boundary must commit before trying to render. ' +
341 - 'This is probably a bug in React.',
342 - );
343 - current.memoizedState = retryCache;
344 - }
345 - // Memoize using the boundary fiber to prevent redundant listeners.
346 - if (!retryCache.has(thenable)) {
347 - retryCache.add(thenable);
348 - let retry = resolveRetryThenable.bind(null, workInProgress, thenable);
349 - if (enableSchedulerTracing) {
350 - retry = Schedule_tracing_wrap(retry);
351 - }
352 - thenable.then(retry, retry);
353 - }
354 - workInProgress.effectTag |= ShouldCapture;
355 - workInProgress.expirationTime = renderExpirationTime;
318 return;
319 }
320 // This boundary already captured during this render. Continue to the next
packages/react-reconciler/src/ReactFiberUnwindWork.js
+13 -23
@@ -9,6 +9,7 @@
9
10 import type {Fiber} from './ReactFiber';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
12 +import type {SuspenseState} from './ReactFiberSuspenseComponent';
13
14 import {
15 ClassComponent,
@@ -18,7 +19,6 @@ import {
19 ContextProvider,
20 SuspenseComponent,
21 SuspenseListComponent,
21 - DehydratedSuspenseComponent,
22 } from 'shared/ReactWorkTags';
23 import {DidCapture, NoEffect, ShouldCapture} from 'shared/ReactSideEffectTags';
24 import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
@@ -71,6 +71,18 @@ function unwindWork(
71 }
72 case SuspenseComponent: {
73 popSuspenseContext(workInProgress);
74 + if (enableSuspenseServerRenderer) {
75 + const suspenseState: null | SuspenseState =
76 + workInProgress.memoizedState;
77 + if (suspenseState !== null && suspenseState.dehydrated !== null) {
78 + invariant(
79 + workInProgress.alternate !== null,
80 + 'Threw in newly mounted dehydrated component. This is likely a bug in ' +
81 + 'React. Please file an issue.',
82 + );
83 + resetHydrationState();
84 + }
85 + }
86 const effectTag = workInProgress.effectTag;
87 if (effectTag & ShouldCapture) {
88 workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
@@ -79,23 +91,6 @@ function unwindWork(
91 }
92 return null;
93 }
82 - case DehydratedSuspenseComponent: {
83 - if (enableSuspenseServerRenderer) {
84 - popSuspenseContext(workInProgress);
85 - if (workInProgress.alternate === null) {
86 - // TODO: popHydrationState
87 - } else {
88 - resetHydrationState();
89 - }
90 - const effectTag = workInProgress.effectTag;
91 - if (effectTag & ShouldCapture) {
92 - workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
93 - // Captured a suspense effect. Re-render the boundary.
94 - return workInProgress;
95 - }
96 - }
97 - return null;
98 - }
94 case SuspenseListComponent: {
95 popSuspenseContext(workInProgress);
96 // SuspenseList doesn't actually catch anything. It should've been
@@ -137,11 +132,6 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
132 case SuspenseComponent:
133 popSuspenseContext(interruptedWork);
134 break;
140 - case DehydratedSuspenseComponent:
141 - if (enableSuspenseServerRenderer) {
142 - popSuspenseContext(interruptedWork);
143 - }
144 - break;
135 case SuspenseListComponent:
136 popSuspenseContext(interruptedWork);
137 break;
packages/react-reconciler/src/ReactFiberWorkLoop.js
-4
@@ -76,7 +76,6 @@ import {
76 HostRoot,
77 ClassComponent,
78 SuspenseComponent,
79 - DehydratedSuspenseComponent,
79 FunctionComponent,
80 ForwardRef,
81 MemoComponent,
@@ -2208,9 +2207,6 @@ export function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) {
2207 case SuspenseComponent:
2208 retryCache = boundaryFiber.stateNode;
2209 break;
2211 - case DehydratedSuspenseComponent:
2212 - retryCache = boundaryFiber.memoizedState;
2213 - break;
2210 default:
2211 invariant(
2212 false,
packages/shared/ReactWorkTags.js
+1 -1
@@ -48,6 +48,6 @@ export const MemoComponent = 14;
48 export const SimpleMemoComponent = 15;
49 export const LazyComponent = 16;
50 export const IncompleteClassComponent = 17;
51 -export const DehydratedSuspenseComponent = 18;
51 +export const DehydratedFragment = 18;
52 export const SuspenseListComponent = 19;
53 export const FundamentalComponent = 20;
scripts/error-codes/codes.json
+3 -1
@@ -337,5 +337,7 @@
337 "336": "The \"%s\" event responder cannot be used via the \"useEvent\" hook.",
338 "337": "An invalid event responder was provided to host component",
339 "338": "ReactDOMServer does not yet support the fundamental API.",
340 - "339": "An invalid value was used as an event listener. Expect one or many event listeners created via React.unstable_useResponder()."
340 + "339": "An invalid value was used as an event listener. Expect one or many event listeners created via React.unstable_useResponder().",
341 + "340": "Threw in newly mounted dehydrated component. This is likely a bug in React. Please file an issue.",
342 + "341": "We just came from a parent so we must have had a parent. This is a bug in React."
343 }