@samitouri / QOS-React / commits / 02b65fd8c5

Allow updates at lower pri without forcing client render

Currently, if a root is updated before the shell has finished hydrating (for example, due to a top-level navigation), we immediately revert to client rendering. This is rare because the root is expected is finish quickly, but not exceedingly rare because the root may be suspended. This adds support for updating the root without forcing a client render as long as the update has lower priority than the initial hydration, i.e. if the update is wrapped in startTransition. To implement this, I had to do some refactoring. The main idea here is to make it closer to how we implement hydration in Suspense boundaries: - I moved isDehydrated from the shared FiberRoot object to the HostRoot's state object. - In the begin phase, I check if the root has received an by comparing the new children to the initial children. If they are different, we revert to client rendering, and set isDehydrated to false using a derived state update (a la getDerivedStateFromProps). - There are a few places where we used to set root.isDehydrated to false as a way to force a client render. Instead, I set the ForceClientRender flag on the root work-in-progress fiber. - Whenever we fall back to client rendering, I log a recoverable error. The overall code structure is almost identical to the corresponding logic for Suspense components. The reason this works is because if the update has lower priority than the initial hydration, it won't be processed during the hydration render, so the children will be the same. We can go even further and allow updates at _higher_ priority (though not sync) by implementing selective hydration at the root, like we do for Suspense boundaries: interrupt the current render, attempt hydration at slightly higher priority than the update, then continue rendering the update. I haven't implemented this yet, but I've structured the code in anticipation of adding this later.

Andrew Clark committed Mar 11, 2022 at 19:48 UTC 02b65fd8c5dbc6bfe2c976841f2f70a593ac9129
15 files changed +435 -233
packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
+34 -2
@@ -9,6 +9,7 @@
9
10 let JSDOM;
11 let React;
12 +let startTransition;
13 let ReactDOMClient;
14 let Scheduler;
15 let clientAct;
@@ -33,6 +34,8 @@ describe('ReactDOMFizzShellHydration', () => {
34 ReactDOMFizzServer = require('react-dom/server');
35 Stream = require('stream');
36
37 + startTransition = React.startTransition;
38 +
39 textCache = new Map();
40
41 // Test Environment
@@ -214,7 +217,36 @@ describe('ReactDOMFizzShellHydration', () => {
217 expect(container.textContent).toBe('Shell');
218 });
219
217 - test('updating the root before the shell hydrates forces a client render', async () => {
220 + test(
221 + 'updating the root at lower priority than initial hydration does not ' +
222 + 'force a client render',
223 + async () => {
224 + function App() {
225 + return <Text text="Initial" />;
226 + }
227 +
228 + // Server render
229 + await resolveText('Initial');
230 + await serverAct(async () => {
231 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
232 + pipe(writable);
233 + });
234 + expect(Scheduler).toHaveYielded(['Initial']);
235 +
236 + await clientAct(async () => {
237 + const root = ReactDOMClient.hydrateRoot(container, <App />);
238 + // This has lower priority than the initial hydration, so the update
239 + // won't be processed until after hydration finishes.
240 + startTransition(() => {
241 + root.render(<Text text="Updated" />);
242 + });
243 + });
244 + expect(Scheduler).toHaveYielded(['Initial', 'Updated']);
245 + expect(container.textContent).toBe('Updated');
246 + },
247 + );
248 +
249 + test('updating the root while the shell is suspended forces a client render', async () => {
250 function App() {
251 return <AsyncText text="Shell" />;
252 }
@@ -245,9 +277,9 @@ describe('ReactDOMFizzShellHydration', () => {
277 root.render(<Text text="New screen" />);
278 });
279 expect(Scheduler).toHaveYielded([
280 + 'New screen',
281 'This root received an early update, before anything was able ' +
282 'hydrate. Switched the entire root to client rendering.',
250 - 'New screen',
283 ]);
284 expect(container.textContent).toBe('New screen');
285 });
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+115 -45
@@ -7,7 +7,11 @@
7 * @flow
8 */
9
10 -import type {ReactProviderType, ReactContext} from 'shared/ReactTypes';
10 +import type {
11 + ReactProviderType,
12 + ReactContext,
13 + ReactNodeList,
14 +} from 'shared/ReactTypes';
15 import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
16 import type {Fiber, FiberRoot} from './ReactInternalTypes';
17 import type {TypeOfMode} from './ReactTypeOfMode';
@@ -29,6 +33,7 @@ import type {
33 SpawnedCachePool,
34 } from './ReactFiberCacheComponent.new';
35 import type {UpdateQueue} from './ReactUpdateQueue.new';
36 +import type {RootState} from './ReactFiberRoot.new';
37 import {
38 enableSuspenseAvoidThisFallback,
39 enableCPUSuspense,
@@ -223,7 +228,6 @@ import {
228 createOffscreenHostContainerFiber,
229 isSimpleFunctionComponent,
230 } from './ReactFiber.new';
226 -import {isRootDehydrated} from './ReactFiberShellHydration';
231 import {
232 retryDehydratedSuspenseBoundary,
233 scheduleUpdateOnFiber,
@@ -1312,7 +1316,7 @@ function pushHostRootContext(workInProgress) {
1316
1317 function updateHostRoot(current, workInProgress, renderLanes) {
1318 pushHostRootContext(workInProgress);
1315 - const updateQueue = workInProgress.updateQueue;
1319 + const updateQueue: UpdateQueue<RootState> = (workInProgress.updateQueue: any);
1320
1321 if (current === null || updateQueue === null) {
1322 throw new Error(
@@ -1327,7 +1331,7 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1331 const prevChildren = prevState.element;
1332 cloneUpdateQueue(current, workInProgress);
1333 processUpdateQueue(workInProgress, nextProps, null, renderLanes);
1330 - const nextState = workInProgress.memoizedState;
1334 + const nextState: RootState = workInProgress.memoizedState;
1335
1336 const root: FiberRoot = workInProgress.stateNode;
1337
@@ -1342,64 +1346,130 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1346 }
1347
1348 if (enableTransitionTracing) {
1349 + // FIXME: Slipped past code review. This is not a safe mutation:
1350 + // workInProgress.memoizedState is a shared object. Need to fix before
1351 + // rolling out the Transition Tracing experiment.
1352 workInProgress.memoizedState.transitions = getWorkInProgressTransitions();
1353 }
1354
1355 // Caution: React DevTools currently depends on this property
1356 // being called "element".
1357 const nextChildren = nextState.element;
1351 - if (nextChildren === prevChildren) {
1352 - resetHydrationState();
1353 - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1354 - }
1355 - if (isRootDehydrated(root) && enterHydrationState(workInProgress)) {
1356 - // If we don't have any current children this might be the first pass.
1357 - // We always try to hydrate. If this isn't a hydration pass there won't
1358 - // be any children to hydrate which is effectively the same thing as
1359 - // not hydrating.
1360 -
1361 - if (supportsHydration) {
1362 - const mutableSourceEagerHydrationData =
1363 - root.mutableSourceEagerHydrationData;
1364 - if (mutableSourceEagerHydrationData != null) {
1365 - for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1366 - const mutableSource = ((mutableSourceEagerHydrationData[
1367 - i
1368 - ]: any): MutableSource<any>);
1369 - const version = mutableSourceEagerHydrationData[i + 1];
1370 - setWorkInProgressVersion(mutableSource, version);
1358 + if (supportsHydration && prevState.isDehydrated) {
1359 + // This is a hydration root whose shell has not yet hydrated. We should
1360 + // attempt to hydrate.
1361 + if (workInProgress.flags & ForceClientRender) {
1362 + // Something errored during a previous attempt to hydrate the shell, so we
1363 + // forced a client render.
1364 + const recoverableError = new Error(
1365 + 'There was an error while hydrating. Because the error happened outside ' +
1366 + 'of a Suspense boundary, the entire root will switch to ' +
1367 + 'client rendering.',
1368 + );
1369 + return mountHostRootWithoutHydrating(
1370 + current,
1371 + workInProgress,
1372 + updateQueue,
1373 + nextState,
1374 + nextChildren,
1375 + renderLanes,
1376 + recoverableError,
1377 + );
1378 + } else if (nextChildren !== prevChildren) {
1379 + const recoverableError = new Error(
1380 + 'This root received an early update, before anything was able ' +
1381 + 'hydrate. Switched the entire root to client rendering.',
1382 + );
1383 + return mountHostRootWithoutHydrating(
1384 + current,
1385 + workInProgress,
1386 + updateQueue,
1387 + nextState,
1388 + nextChildren,
1389 + renderLanes,
1390 + recoverableError,
1391 + );
1392 + } else {
1393 + // The outermost shell has not hydrated yet. Start hydrating.
1394 + enterHydrationState(workInProgress);
1395 + if (supportsHydration) {
1396 + const mutableSourceEagerHydrationData =
1397 + root.mutableSourceEagerHydrationData;
1398 + if (mutableSourceEagerHydrationData != null) {
1399 + for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1400 + const mutableSource = ((mutableSourceEagerHydrationData[
1401 + i
1402 + ]: any): MutableSource<any>);
1403 + const version = mutableSourceEagerHydrationData[i + 1];
1404 + setWorkInProgressVersion(mutableSource, version);
1405 + }
1406 }
1407 }
1373 - }
1408
1375 - const child = mountChildFibers(
1376 - workInProgress,
1377 - null,
1378 - nextChildren,
1379 - renderLanes,
1380 - );
1381 - workInProgress.child = child;
1409 + const child = mountChildFibers(
1410 + workInProgress,
1411 + null,
1412 + nextChildren,
1413 + renderLanes,
1414 + );
1415 + workInProgress.child = child;
1416
1383 - let node = child;
1384 - while (node) {
1385 - // Mark each child as hydrating. This is a fast path to know whether this
1386 - // tree is part of a hydrating tree. This is used to determine if a child
1387 - // node has fully mounted yet, and for scheduling event replaying.
1388 - // Conceptually this is similar to Placement in that a new subtree is
1389 - // inserted into the React tree here. It just happens to not need DOM
1390 - // mutations because it already exists.
1391 - node.flags = (node.flags & ~Placement) | Hydrating;
1392 - node = node.sibling;
1417 + let node = child;
1418 + while (node) {
1419 + // Mark each child as hydrating. This is a fast path to know whether this
1420 + // tree is part of a hydrating tree. This is used to determine if a child
1421 + // node has fully mounted yet, and for scheduling event replaying.
1422 + // Conceptually this is similar to Placement in that a new subtree is
1423 + // inserted into the React tree here. It just happens to not need DOM
1424 + // mutations because it already exists.
1425 + node.flags = (node.flags & ~Placement) | Hydrating;
1426 + node = node.sibling;
1427 + }
1428 }
1429 } else {
1395 - // Otherwise reset hydration state in case we aborted and resumed another
1396 - // root.
1397 - reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1430 + // Root is not dehydrated. Either this is a client-only root, or it
1431 + // already hydrated.
1432 resetHydrationState();
1433 + if (nextChildren === prevChildren) {
1434 + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1435 + }
1436 + reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1437 }
1438 return workInProgress.child;
1439 }
1440
1441 +function mountHostRootWithoutHydrating(
1442 + current: Fiber,
1443 + workInProgress: Fiber,
1444 + updateQueue: UpdateQueue<RootState>,
1445 + nextState: RootState,
1446 + nextChildren: ReactNodeList,
1447 + renderLanes: Lanes,
1448 + recoverableError: Error,
1449 +) {
1450 + // Revert to client rendering.
1451 + resetHydrationState();
1452 +
1453 + queueHydrationError(recoverableError);
1454 +
1455 + workInProgress.flags |= ForceClientRender;
1456 +
1457 + // Flip isDehydrated to false to indicate that when this render
1458 + // finishes, the root will no longer be dehydrated.
1459 + const overrideState: RootState = {
1460 + element: nextChildren,
1461 + isDehydrated: false,
1462 + cache: nextState.cache,
1463 + transitions: nextState.transitions,
1464 + };
1465 + // `baseState` can always be the last state because the root doesn't
1466 + // have reducer functions so it doesn't need rebasing.
1467 + updateQueue.baseState = overrideState;
1468 + workInProgress.memoizedState = overrideState;
1469 + reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1470 + return workInProgress.child;
1471 +}
1472 +
1473 function updateHostComponent(
1474 current: Fiber | null,
1475 workInProgress: Fiber,
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+115 -45
@@ -7,7 +7,11 @@
7 * @flow
8 */
9
10 -import type {ReactProviderType, ReactContext} from 'shared/ReactTypes';
10 +import type {
11 + ReactProviderType,
12 + ReactContext,
13 + ReactNodeList,
14 +} from 'shared/ReactTypes';
15 import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
16 import type {Fiber, FiberRoot} from './ReactInternalTypes';
17 import type {TypeOfMode} from './ReactTypeOfMode';
@@ -29,6 +33,7 @@ import type {
33 SpawnedCachePool,
34 } from './ReactFiberCacheComponent.old';
35 import type {UpdateQueue} from './ReactUpdateQueue.old';
36 +import type {RootState} from './ReactFiberRoot.old';
37 import {
38 enableSuspenseAvoidThisFallback,
39 enableCPUSuspense,
@@ -223,7 +228,6 @@ import {
228 createOffscreenHostContainerFiber,
229 isSimpleFunctionComponent,
230 } from './ReactFiber.old';
226 -import {isRootDehydrated} from './ReactFiberShellHydration';
231 import {
232 retryDehydratedSuspenseBoundary,
233 scheduleUpdateOnFiber,
@@ -1312,7 +1316,7 @@ function pushHostRootContext(workInProgress) {
1316
1317 function updateHostRoot(current, workInProgress, renderLanes) {
1318 pushHostRootContext(workInProgress);
1315 - const updateQueue = workInProgress.updateQueue;
1319 + const updateQueue: UpdateQueue<RootState> = (workInProgress.updateQueue: any);
1320
1321 if (current === null || updateQueue === null) {
1322 throw new Error(
@@ -1327,7 +1331,7 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1331 const prevChildren = prevState.element;
1332 cloneUpdateQueue(current, workInProgress);
1333 processUpdateQueue(workInProgress, nextProps, null, renderLanes);
1330 - const nextState = workInProgress.memoizedState;
1334 + const nextState: RootState = workInProgress.memoizedState;
1335
1336 const root: FiberRoot = workInProgress.stateNode;
1337
@@ -1342,64 +1346,130 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1346 }
1347
1348 if (enableTransitionTracing) {
1349 + // FIXME: Slipped past code review. This is not a safe mutation:
1350 + // workInProgress.memoizedState is a shared object. Need to fix before
1351 + // rolling out the Transition Tracing experiment.
1352 workInProgress.memoizedState.transitions = getWorkInProgressTransitions();
1353 }
1354
1355 // Caution: React DevTools currently depends on this property
1356 // being called "element".
1357 const nextChildren = nextState.element;
1351 - if (nextChildren === prevChildren) {
1352 - resetHydrationState();
1353 - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1354 - }
1355 - if (isRootDehydrated(root) && enterHydrationState(workInProgress)) {
1356 - // If we don't have any current children this might be the first pass.
1357 - // We always try to hydrate. If this isn't a hydration pass there won't
1358 - // be any children to hydrate which is effectively the same thing as
1359 - // not hydrating.
1360 -
1361 - if (supportsHydration) {
1362 - const mutableSourceEagerHydrationData =
1363 - root.mutableSourceEagerHydrationData;
1364 - if (mutableSourceEagerHydrationData != null) {
1365 - for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1366 - const mutableSource = ((mutableSourceEagerHydrationData[
1367 - i
1368 - ]: any): MutableSource<any>);
1369 - const version = mutableSourceEagerHydrationData[i + 1];
1370 - setWorkInProgressVersion(mutableSource, version);
1358 + if (supportsHydration && prevState.isDehydrated) {
1359 + // This is a hydration root whose shell has not yet hydrated. We should
1360 + // attempt to hydrate.
1361 + if (workInProgress.flags & ForceClientRender) {
1362 + // Something errored during a previous attempt to hydrate the shell, so we
1363 + // forced a client render.
1364 + const recoverableError = new Error(
1365 + 'There was an error while hydrating. Because the error happened outside ' +
1366 + 'of a Suspense boundary, the entire root will switch to ' +
1367 + 'client rendering.',
1368 + );
1369 + return mountHostRootWithoutHydrating(
1370 + current,
1371 + workInProgress,
1372 + updateQueue,
1373 + nextState,
1374 + nextChildren,
1375 + renderLanes,
1376 + recoverableError,
1377 + );
1378 + } else if (nextChildren !== prevChildren) {
1379 + const recoverableError = new Error(
1380 + 'This root received an early update, before anything was able ' +
1381 + 'hydrate. Switched the entire root to client rendering.',
1382 + );
1383 + return mountHostRootWithoutHydrating(
1384 + current,
1385 + workInProgress,
1386 + updateQueue,
1387 + nextState,
1388 + nextChildren,
1389 + renderLanes,
1390 + recoverableError,
1391 + );
1392 + } else {
1393 + // The outermost shell has not hydrated yet. Start hydrating.
1394 + enterHydrationState(workInProgress);
1395 + if (supportsHydration) {
1396 + const mutableSourceEagerHydrationData =
1397 + root.mutableSourceEagerHydrationData;
1398 + if (mutableSourceEagerHydrationData != null) {
1399 + for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1400 + const mutableSource = ((mutableSourceEagerHydrationData[
1401 + i
1402 + ]: any): MutableSource<any>);
1403 + const version = mutableSourceEagerHydrationData[i + 1];
1404 + setWorkInProgressVersion(mutableSource, version);
1405 + }
1406 }
1407 }
1373 - }
1408
1375 - const child = mountChildFibers(
1376 - workInProgress,
1377 - null,
1378 - nextChildren,
1379 - renderLanes,
1380 - );
1381 - workInProgress.child = child;
1409 + const child = mountChildFibers(
1410 + workInProgress,
1411 + null,
1412 + nextChildren,
1413 + renderLanes,
1414 + );
1415 + workInProgress.child = child;
1416
1383 - let node = child;
1384 - while (node) {
1385 - // Mark each child as hydrating. This is a fast path to know whether this
1386 - // tree is part of a hydrating tree. This is used to determine if a child
1387 - // node has fully mounted yet, and for scheduling event replaying.
1388 - // Conceptually this is similar to Placement in that a new subtree is
1389 - // inserted into the React tree here. It just happens to not need DOM
1390 - // mutations because it already exists.
1391 - node.flags = (node.flags & ~Placement) | Hydrating;
1392 - node = node.sibling;
1417 + let node = child;
1418 + while (node) {
1419 + // Mark each child as hydrating. This is a fast path to know whether this
1420 + // tree is part of a hydrating tree. This is used to determine if a child
1421 + // node has fully mounted yet, and for scheduling event replaying.
1422 + // Conceptually this is similar to Placement in that a new subtree is
1423 + // inserted into the React tree here. It just happens to not need DOM
1424 + // mutations because it already exists.
1425 + node.flags = (node.flags & ~Placement) | Hydrating;
1426 + node = node.sibling;
1427 + }
1428 }
1429 } else {
1395 - // Otherwise reset hydration state in case we aborted and resumed another
1396 - // root.
1397 - reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1430 + // Root is not dehydrated. Either this is a client-only root, or it
1431 + // already hydrated.
1432 resetHydrationState();
1433 + if (nextChildren === prevChildren) {
1434 + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1435 + }
1436 + reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1437 }
1438 return workInProgress.child;
1439 }
1440
1441 +function mountHostRootWithoutHydrating(
1442 + current: Fiber,
1443 + workInProgress: Fiber,
1444 + updateQueue: UpdateQueue<RootState>,
1445 + nextState: RootState,
1446 + nextChildren: ReactNodeList,
1447 + renderLanes: Lanes,
1448 + recoverableError: Error,
1449 +) {
1450 + // Revert to client rendering.
1451 + resetHydrationState();
1452 +
1453 + queueHydrationError(recoverableError);
1454 +
1455 + workInProgress.flags |= ForceClientRender;
1456 +
1457 + // Flip isDehydrated to false to indicate that when this render
1458 + // finishes, the root will no longer be dehydrated.
1459 + const overrideState: RootState = {
1460 + element: nextChildren,
1461 + isDehydrated: false,
1462 + cache: nextState.cache,
1463 + transitions: nextState.transitions,
1464 + };
1465 + // `baseState` can always be the last state because the root doesn't
1466 + // have reducer functions so it doesn't need rebasing.
1467 + updateQueue.baseState = overrideState;
1468 + workInProgress.memoizedState = overrideState;
1469 + reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1470 + return workInProgress.child;
1471 +}
1472 +
1473 function updateHostComponent(
1474 current: Fiber | null,
1475 workInProgress: Fiber,
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+13 -11
@@ -25,6 +25,7 @@ import type {Wakeable} from 'shared/ReactTypes';
25 import type {OffscreenState} from './ReactFiberOffscreenComponent';
26 import type {HookFlags} from './ReactHookEffectTags';
27 import type {Cache} from './ReactFiberCacheComponent.new';
28 +import type {RootState} from './ReactFiberRoot.new';
29
30 import {
31 enableCreateEventHandleAPI,
@@ -82,7 +83,6 @@ import {
83 Visibility,
84 } from './ReactFiberFlags';
85 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
85 -import {isRootDehydrated} from './ReactFiberShellHydration';
86 import {
87 resetCurrentFiber as resetCurrentDebugFiberInDEV,
88 setCurrentFiber as setCurrentDebugFiberInDEV,
@@ -1878,11 +1878,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1878 }
1879 case HostRoot: {
1880 if (supportsHydration) {
1881 - const root: FiberRoot = finishedWork.stateNode;
1882 - if (isRootDehydrated(root)) {
1883 - // We've just hydrated. No need to hydrate again.
1884 - root.isDehydrated = false;
1885 - commitHydratedContainer(root.containerInfo);
1881 + if (current !== null) {
1882 + const prevRootState: RootState = current.memoizedState;
1883 + if (prevRootState.isDehydrated) {
1884 + const root: FiberRoot = finishedWork.stateNode;
1885 + commitHydratedContainer(root.containerInfo);
1886 + }
1887 }
1888 }
1889 break;
@@ -1986,11 +1987,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1987 }
1988 case HostRoot: {
1989 if (supportsHydration) {
1989 - const root: FiberRoot = finishedWork.stateNode;
1990 - if (isRootDehydrated(root)) {
1991 - // We've just hydrated. No need to hydrate again.
1992 - root.isDehydrated = false;
1993 - commitHydratedContainer(root.containerInfo);
1990 + if (current !== null) {
1991 + const prevRootState: RootState = current.memoizedState;
1992 + if (prevRootState.isDehydrated) {
1993 + const root: FiberRoot = finishedWork.stateNode;
1994 + commitHydratedContainer(root.containerInfo);
1995 + }
1996 }
1997 }
1998 return;
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+13 -11
@@ -25,6 +25,7 @@ import type {Wakeable} from 'shared/ReactTypes';
25 import type {OffscreenState} from './ReactFiberOffscreenComponent';
26 import type {HookFlags} from './ReactHookEffectTags';
27 import type {Cache} from './ReactFiberCacheComponent.old';
28 +import type {RootState} from './ReactFiberRoot.old';
29
30 import {
31 enableCreateEventHandleAPI,
@@ -82,7 +83,6 @@ import {
83 Visibility,
84 } from './ReactFiberFlags';
85 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
85 -import {isRootDehydrated} from './ReactFiberShellHydration';
86 import {
87 resetCurrentFiber as resetCurrentDebugFiberInDEV,
88 setCurrentFiber as setCurrentDebugFiberInDEV,
@@ -1878,11 +1878,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1878 }
1879 case HostRoot: {
1880 if (supportsHydration) {
1881 - const root: FiberRoot = finishedWork.stateNode;
1882 - if (isRootDehydrated(root)) {
1883 - // We've just hydrated. No need to hydrate again.
1884 - root.isDehydrated = false;
1885 - commitHydratedContainer(root.containerInfo);
1881 + if (current !== null) {
1882 + const prevRootState: RootState = current.memoizedState;
1883 + if (prevRootState.isDehydrated) {
1884 + const root: FiberRoot = finishedWork.stateNode;
1885 + commitHydratedContainer(root.containerInfo);
1886 + }
1887 }
1888 }
1889 break;
@@ -1986,11 +1987,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1987 }
1988 case HostRoot: {
1989 if (supportsHydration) {
1989 - const root: FiberRoot = finishedWork.stateNode;
1990 - if (isRootDehydrated(root)) {
1991 - // We've just hydrated. No need to hydrate again.
1992 - root.isDehydrated = false;
1993 - commitHydratedContainer(root.containerInfo);
1990 + if (current !== null) {
1991 + const prevRootState: RootState = current.memoizedState;
1992 + if (prevRootState.isDehydrated) {
1993 + const root: FiberRoot = finishedWork.stateNode;
1994 + commitHydratedContainer(root.containerInfo);
1995 + }
1996 }
1997 }
1998 return;
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+24 -7
@@ -8,6 +8,7 @@
8 */
9
10 import type {Fiber} from './ReactInternalTypes';
11 +import type {RootState} from './ReactFiberRoot.new';
12 import type {Lanes, Lane} from './ReactFiberLane.new';
13 import type {
14 ReactScopeInstance,
@@ -160,7 +161,6 @@ import {
161 includesSomeLane,
162 mergeLanes,
163 } from './ReactFiberLane.new';
163 -import {isRootDehydrated} from './ReactFiberShellHydration';
164 import {resetChildFibers} from './ReactChildFiber.new';
165 import {createScopeInstance} from './ReactFiberScope.new';
166 import {transferActualDuration} from './ReactProfilerTimer.new';
@@ -891,12 +891,29 @@ function completeWork(
891 // If we hydrated, then we'll need to schedule an update for
892 // the commit side-effects on the root.
893 markUpdate(workInProgress);
894 - } else if (!isRootDehydrated(fiberRoot)) {
895 - // Schedule an effect to clear this container at the start of the next commit.
896 - // This handles the case of React rendering into a container with previous children.
897 - // It's also safe to do for updates too, because current.child would only be null
898 - // if the previous render was null (so the container would already be empty).
899 - workInProgress.flags |= Snapshot;
894 + } else {
895 + if (current !== null) {
896 + const prevState: RootState = current.memoizedState;
897 + if (
898 + // Check if this is a client root
899 + !prevState.isDehydrated ||
900 + // Check if we reverted to client rendering (e.g. due to an error)
901 + (workInProgress.flags & ForceClientRender) !== NoFlags
902 + ) {
903 + // Schedule an effect to clear this container at the start of the
904 + // next commit. This handles the case of React rendering into a
905 + // container with previous children. It's also safe to do for
906 + // updates too, because current.child would only be null if the
907 + // previous render was null (so the container would already
908 + // be empty).
909 + workInProgress.flags |= Snapshot;
910 +
911 + // If this was a forced client render, there may have been
912 + // recoverable errors during first hydration attempt. If so, add
913 + // them to a queue so we can log them in the commit phase.
914 + upgradeHydrationErrorsToRecoverable();
915 + }
916 + }
917 }
918 }
919 updateHostContainer(current, workInProgress);
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+24 -7
@@ -8,6 +8,7 @@
8 */
9
10 import type {Fiber} from './ReactInternalTypes';
11 +import type {RootState} from './ReactFiberRoot.old';
12 import type {Lanes, Lane} from './ReactFiberLane.old';
13 import type {
14 ReactScopeInstance,
@@ -160,7 +161,6 @@ import {
161 includesSomeLane,
162 mergeLanes,
163 } from './ReactFiberLane.old';
163 -import {isRootDehydrated} from './ReactFiberShellHydration';
164 import {resetChildFibers} from './ReactChildFiber.old';
165 import {createScopeInstance} from './ReactFiberScope.old';
166 import {transferActualDuration} from './ReactProfilerTimer.old';
@@ -891,12 +891,29 @@ function completeWork(
891 // If we hydrated, then we'll need to schedule an update for
892 // the commit side-effects on the root.
893 markUpdate(workInProgress);
894 - } else if (!isRootDehydrated(fiberRoot)) {
895 - // Schedule an effect to clear this container at the start of the next commit.
896 - // This handles the case of React rendering into a container with previous children.
897 - // It's also safe to do for updates too, because current.child would only be null
898 - // if the previous render was null (so the container would already be empty).
899 - workInProgress.flags |= Snapshot;
894 + } else {
895 + if (current !== null) {
896 + const prevState: RootState = current.memoizedState;
897 + if (
898 + // Check if this is a client root
899 + !prevState.isDehydrated ||
900 + // Check if we reverted to client rendering (e.g. due to an error)
901 + (workInProgress.flags & ForceClientRender) !== NoFlags
902 + ) {
903 + // Schedule an effect to clear this container at the start of the
904 + // next commit. This handles the case of React rendering into a
905 + // container with previous children. It's also safe to do for
906 + // updates too, because current.child would only be null if the
907 + // previous render was null (so the container would already
908 + // be empty).
909 + workInProgress.flags |= Snapshot;
910 +
911 + // If this was a forced client render, there may have been
912 + // recoverable errors during first hydration attempt. If so, add
913 + // them to a queue so we can log them in the commit phase.
914 + upgradeHydrationErrorsToRecoverable();
915 + }
916 + }
917 }
918 }
919 updateHostContainer(current, workInProgress);
packages/react-reconciler/src/ReactFiberReconciler.new.js
+4 -3
@@ -254,10 +254,12 @@ export function createContainer(
254 transitionCallbacks: null | TransitionTracingCallbacks,
255 ): OpaqueRoot {
256 const hydrate = false;
257 + const initialChildren = null;
258 return createFiberRoot(
259 containerInfo,
260 tag,
261 hydrate,
262 + initialChildren,
263 hydrationCallbacks,
264 isStrictMode,
265 concurrentUpdatesByDefaultOverride,
@@ -285,6 +287,7 @@ export function createHydrationContainer(
287 containerInfo,
288 tag,
289 hydrate,
290 + initialChildren,
291 hydrationCallbacks,
292 isStrictMode,
293 concurrentUpdatesByDefaultOverride,
@@ -303,9 +306,7 @@ export function createHydrationContainer(
306 const eventTime = requestEventTime();
307 const lane = requestUpdateLane(current);
308 const update = createUpdate(eventTime, lane);
306 - // Caution: React DevTools currently depends on this property
307 - // being called "element".
308 - update.payload = {element: initialChildren};
309 + update.payload = {isDehydrated: false};
310 update.callback =
311 callback !== undefined && callback !== null ? callback : null;
312 enqueueUpdate(current, update, lane);
packages/react-reconciler/src/ReactFiberReconciler.old.js
+4 -3
@@ -254,10 +254,12 @@ export function createContainer(
254 transitionCallbacks: null | TransitionTracingCallbacks,
255 ): OpaqueRoot {
256 const hydrate = false;
257 + const initialChildren = null;
258 return createFiberRoot(
259 containerInfo,
260 tag,
261 hydrate,
262 + initialChildren,
263 hydrationCallbacks,
264 isStrictMode,
265 concurrentUpdatesByDefaultOverride,
@@ -285,6 +287,7 @@ export function createHydrationContainer(
287 containerInfo,
288 tag,
289 hydrate,
290 + initialChildren,
291 hydrationCallbacks,
292 isStrictMode,
293 concurrentUpdatesByDefaultOverride,
@@ -303,9 +306,7 @@ export function createHydrationContainer(
306 const eventTime = requestEventTime();
307 const lane = requestUpdateLane(current);
308 const update = createUpdate(eventTime, lane);
306 - // Caution: React DevTools currently depends on this property
307 - // being called "element".
308 - update.payload = {element: initialChildren};
309 + update.payload = {isDehydrated: false};
310 update.callback =
311 callback !== undefined && callback !== null ? callback : null;
312 enqueueUpdate(current, update, lane);
packages/react-reconciler/src/ReactFiberRoot.new.js
+9 -5
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {ReactNodeList} from 'shared/ReactTypes';
11 import type {
12 FiberRoot,
13 SuspenseHydrationCallbacks,
@@ -39,7 +40,8 @@ import {createCache, retainCache} from './ReactFiberCacheComponent.new';
40
41 export type RootState = {
42 element: any,
42 - cache: Cache | null,
43 + isDehydrated: boolean,
44 + cache: Cache,
45 transitions: Transitions | null,
46 };
47
@@ -59,7 +61,6 @@ function FiberRootNode(
61 this.timeoutHandle = noTimeout;
62 this.context = null;
63 this.pendingContext = null;
62 - this.isDehydrated = hydrate;
64 this.callbackNode = null;
65 this.callbackPriority = NoLane;
66 this.eventTimes = createLaneMap(NoLanes);
@@ -128,6 +129,7 @@ export function createFiberRoot(
129 containerInfo: any,
130 tag: RootTag,
131 hydrate: boolean,
132 + initialChildren: ReactNodeList,
133 hydrationCallbacks: null | SuspenseHydrationCallbacks,
134 isStrictMode: boolean,
135 concurrentUpdatesByDefaultOverride: null | boolean,
@@ -178,15 +180,17 @@ export function createFiberRoot(
180 root.pooledCache = initialCache;
181 retainCache(initialCache);
182 const initialState: RootState = {
181 - element: null,
183 + element: initialChildren,
184 + isDehydrated: hydrate,
185 cache: initialCache,
186 transitions: null,
187 };
188 uninitializedFiber.memoizedState = initialState;
189 } else {
190 const initialState: RootState = {
188 - element: null,
189 - cache: null,
191 + element: initialChildren,
192 + isDehydrated: hydrate,
193 + cache: (null: any), // not enabled yet
194 transitions: null,
195 };
196 uninitializedFiber.memoizedState = initialState;
packages/react-reconciler/src/ReactFiberRoot.old.js
+9 -5
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {ReactNodeList} from 'shared/ReactTypes';
11 import type {
12 FiberRoot,
13 SuspenseHydrationCallbacks,
@@ -39,7 +40,8 @@ import {createCache, retainCache} from './ReactFiberCacheComponent.old';
40
41 export type RootState = {
42 element: any,
42 - cache: Cache | null,
43 + isDehydrated: boolean,
44 + cache: Cache,
45 transitions: Transitions | null,
46 };
47
@@ -59,7 +61,6 @@ function FiberRootNode(
61 this.timeoutHandle = noTimeout;
62 this.context = null;
63 this.pendingContext = null;
62 - this.isDehydrated = hydrate;
64 this.callbackNode = null;
65 this.callbackPriority = NoLane;
66 this.eventTimes = createLaneMap(NoLanes);
@@ -128,6 +129,7 @@ export function createFiberRoot(
129 containerInfo: any,
130 tag: RootTag,
131 hydrate: boolean,
132 + initialChildren: ReactNodeList,
133 hydrationCallbacks: null | SuspenseHydrationCallbacks,
134 isStrictMode: boolean,
135 concurrentUpdatesByDefaultOverride: null | boolean,
@@ -178,15 +180,17 @@ export function createFiberRoot(
180 root.pooledCache = initialCache;
181 retainCache(initialCache);
182 const initialState: RootState = {
181 - element: null,
183 + element: initialChildren,
184 + isDehydrated: hydrate,
185 cache: initialCache,
186 transitions: null,
187 };
188 uninitializedFiber.memoizedState = initialState;
189 } else {
190 const initialState: RootState = {
188 - element: null,
189 - cache: null,
191 + element: initialChildren,
192 + isDehydrated: hydrate,
193 + cache: (null: any), // not enabled yet
194 transitions: null,
195 };
196 uninitializedFiber.memoizedState = initialState;
packages/react-reconciler/src/ReactFiberShellHydration.js
+3 -1
@@ -8,10 +8,12 @@
8 */
9
10 import type {FiberRoot} from './ReactInternalTypes';
11 +import type {RootState} from './ReactFiberRoot.new';
12
13 // This is imported by the event replaying implementation in React DOM. It's
14 // in a separate file to break a circular dependency between the renderer and
15 // the reconciler.
16 export function isRootDehydrated(root: FiberRoot) {
16 - return root.isDehydrated;
17 + const currentState: RootState = root.current.memoizedState;
18 + return currentState.isDehydrated;
19 }
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+34 -43
@@ -110,6 +110,7 @@ import {
110 StoreConsistency,
111 HostEffectMask,
112 Hydrating,
113 + ForceClientRender,
114 BeforeMutationMask,
115 MutationMask,
116 LayoutMask,
@@ -582,34 +583,7 @@ export function scheduleUpdateOnFiber(
583 }
584 }
585
585 - if (isRootDehydrated(root) && root.tag !== LegacyRoot) {
586 - // This root's shell hasn't hydrated yet. Revert to client rendering.
587 - if (workInProgressRoot === root) {
588 - // If this happened during an interleaved event, interrupt the
589 - // in-progress hydration. Theoretically, we could attempt to force a
590 - // synchronous hydration before switching to client rendering, but the
591 - // most common reason the shell hasn't hydrated yet is because it
592 - // suspended. So it's very likely to suspend again anyway. For
593 - // simplicity, we'll skip that atttempt and go straight to
594 - // client rendering.
595 - //
596 - // Another way to model this would be to give the initial hydration its
597 - // own special lane. However, it may not be worth adding a lane solely
598 - // for this purpose, so we'll wait until we find another use case before
599 - // adding it.
600 - //
601 - // TODO: Consider only interrupting hydration if the priority of the
602 - // update is higher than default.
603 - prepareFreshStack(root, NoLanes);
604 - }
605 - root.isDehydrated = false;
606 - const error = new Error(
607 - 'This root received an early update, before anything was able ' +
608 - 'hydrate. Switched the entire root to client rendering.',
609 - );
610 - const onRecoverableError = root.onRecoverableError;
611 - onRecoverableError(error);
612 - } else if (root === workInProgressRoot) {
586 + if (root === workInProgressRoot) {
587 // TODO: Consolidate with `isInterleavedUpdate` check
588
589 // Received an update to a tree that's in the middle of rendering. Mark
@@ -1017,28 +991,42 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
991 function recoverFromConcurrentError(root, errorRetryLanes) {
992 // If an error occurred during hydration, discard server response and fall
993 // back to client side render.
994 +
995 + // Before rendering again, save the errors from the previous attempt.
996 + const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
997 +
998 if (isRootDehydrated(root)) {
1021 - root.isDehydrated = false;
999 + // The shell failed to hydrate. Set a flag to force a client rendering
1000 + // during the next attempt. To do this, we call prepareFreshStack now
1001 + // to create the root work-in-progress fiber. This is a bit weird in terms
1002 + // of factoring, because it relies on renderRootSync not calling
1003 + // prepareFreshStack again in the call below, which happens because the
1004 + // root and lanes haven't changed.
1005 + //
1006 + // TODO: I think what we should do is set ForceClientRender inside
1007 + // throwException, like we do for nested Suspense boundaries. The reason
1008 + // it's here instead is so we can switch to the synchronous work loop, too.
1009 + // Something to consider for a future refactor.
1010 + const rootWorkInProgress = prepareFreshStack(root, errorRetryLanes);
1011 + rootWorkInProgress.flags |= ForceClientRender;
1012 if (__DEV__) {
1013 errorHydratingContainer(root.containerInfo);
1014 }
1025 - const error = new Error(
1026 - 'There was an error while hydrating. Because the error happened outside ' +
1027 - 'of a Suspense boundary, the entire root will switch to ' +
1028 - 'client rendering.',
1029 - );
1030 - renderDidError(error);
1015 }
1016
1033 - const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
1017 const exitStatus = renderRootSync(root, errorRetryLanes);
1018 if (exitStatus !== RootErrored) {
1019 // Successfully finished rendering on retry
1037 - if (errorsFromFirstAttempt !== null) {
1038 - // The errors from the failed first attempt have been recovered. Add
1039 - // them to the collection of recoverable errors. We'll log them in the
1040 - // commit phase.
1041 - queueRecoverableErrors(errorsFromFirstAttempt);
1020 +
1021 + // The errors from the failed first attempt have been recovered. Add
1022 + // them to the collection of recoverable errors. We'll log them in the
1023 + // commit phase.
1024 + const errorsFromSecondAttempt = workInProgressRootRecoverableErrors;
1025 + workInProgressRootRecoverableErrors = errorsFromFirstAttempt;
1026 + // The errors from the second attempt should be queued after the errors
1027 + // from the first attempt, to preserve the causal sequence.
1028 + if (errorsFromSecondAttempt !== null) {
1029 + queueRecoverableErrors(errorsFromSecondAttempt);
1030 }
1031 } else {
1032 // The UI failed to recover.
@@ -1454,7 +1442,7 @@ export function popRenderLanes(fiber: Fiber) {
1442 popFromStack(subtreeRenderLanesCursor, fiber);
1443 }
1444
1457 -function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1445 +function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1446 root.finishedWork = null;
1447 root.finishedLanes = NoLanes;
1448
@@ -1480,7 +1468,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1468 }
1469 }
1470 workInProgressRoot = root;
1483 - workInProgress = createWorkInProgress(root.current, null);
1471 + const rootWorkInProgress = createWorkInProgress(root.current, null);
1472 + workInProgress = rootWorkInProgress;
1473 workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
1474 workInProgressRootExitStatus = RootInProgress;
1475 workInProgressRootFatalError = null;
@@ -1496,6 +1485,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1485 if (__DEV__) {
1486 ReactStrictModeWarnings.discardPendingWarnings();
1487 }
1488 +
1489 + return rootWorkInProgress;
1490 }
1491
1492 function handleError(root, thrownValue): void {
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+34 -43
@@ -110,6 +110,7 @@ import {
110 StoreConsistency,
111 HostEffectMask,
112 Hydrating,
113 + ForceClientRender,
114 BeforeMutationMask,
115 MutationMask,
116 LayoutMask,
@@ -582,34 +583,7 @@ export function scheduleUpdateOnFiber(
583 }
584 }
585
585 - if (isRootDehydrated(root) && root.tag !== LegacyRoot) {
586 - // This root's shell hasn't hydrated yet. Revert to client rendering.
587 - if (workInProgressRoot === root) {
588 - // If this happened during an interleaved event, interrupt the
589 - // in-progress hydration. Theoretically, we could attempt to force a
590 - // synchronous hydration before switching to client rendering, but the
591 - // most common reason the shell hasn't hydrated yet is because it
592 - // suspended. So it's very likely to suspend again anyway. For
593 - // simplicity, we'll skip that atttempt and go straight to
594 - // client rendering.
595 - //
596 - // Another way to model this would be to give the initial hydration its
597 - // own special lane. However, it may not be worth adding a lane solely
598 - // for this purpose, so we'll wait until we find another use case before
599 - // adding it.
600 - //
601 - // TODO: Consider only interrupting hydration if the priority of the
602 - // update is higher than default.
603 - prepareFreshStack(root, NoLanes);
604 - }
605 - root.isDehydrated = false;
606 - const error = new Error(
607 - 'This root received an early update, before anything was able ' +
608 - 'hydrate. Switched the entire root to client rendering.',
609 - );
610 - const onRecoverableError = root.onRecoverableError;
611 - onRecoverableError(error);
612 - } else if (root === workInProgressRoot) {
586 + if (root === workInProgressRoot) {
587 // TODO: Consolidate with `isInterleavedUpdate` check
588
589 // Received an update to a tree that's in the middle of rendering. Mark
@@ -1017,28 +991,42 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
991 function recoverFromConcurrentError(root, errorRetryLanes) {
992 // If an error occurred during hydration, discard server response and fall
993 // back to client side render.
994 +
995 + // Before rendering again, save the errors from the previous attempt.
996 + const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
997 +
998 if (isRootDehydrated(root)) {
1021 - root.isDehydrated = false;
999 + // The shell failed to hydrate. Set a flag to force a client rendering
1000 + // during the next attempt. To do this, we call prepareFreshStack now
1001 + // to create the root work-in-progress fiber. This is a bit weird in terms
1002 + // of factoring, because it relies on renderRootSync not calling
1003 + // prepareFreshStack again in the call below, which happens because the
1004 + // root and lanes haven't changed.
1005 + //
1006 + // TODO: I think what we should do is set ForceClientRender inside
1007 + // throwException, like we do for nested Suspense boundaries. The reason
1008 + // it's here instead is so we can switch to the synchronous work loop, too.
1009 + // Something to consider for a future refactor.
1010 + const rootWorkInProgress = prepareFreshStack(root, errorRetryLanes);
1011 + rootWorkInProgress.flags |= ForceClientRender;
1012 if (__DEV__) {
1013 errorHydratingContainer(root.containerInfo);
1014 }
1025 - const error = new Error(
1026 - 'There was an error while hydrating. Because the error happened outside ' +
1027 - 'of a Suspense boundary, the entire root will switch to ' +
1028 - 'client rendering.',
1029 - );
1030 - renderDidError(error);
1015 }
1016
1033 - const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
1017 const exitStatus = renderRootSync(root, errorRetryLanes);
1018 if (exitStatus !== RootErrored) {
1019 // Successfully finished rendering on retry
1037 - if (errorsFromFirstAttempt !== null) {
1038 - // The errors from the failed first attempt have been recovered. Add
1039 - // them to the collection of recoverable errors. We'll log them in the
1040 - // commit phase.
1041 - queueRecoverableErrors(errorsFromFirstAttempt);
1020 +
1021 + // The errors from the failed first attempt have been recovered. Add
1022 + // them to the collection of recoverable errors. We'll log them in the
1023 + // commit phase.
1024 + const errorsFromSecondAttempt = workInProgressRootRecoverableErrors;
1025 + workInProgressRootRecoverableErrors = errorsFromFirstAttempt;
1026 + // The errors from the second attempt should be queued after the errors
1027 + // from the first attempt, to preserve the causal sequence.
1028 + if (errorsFromSecondAttempt !== null) {
1029 + queueRecoverableErrors(errorsFromSecondAttempt);
1030 }
1031 } else {
1032 // The UI failed to recover.
@@ -1454,7 +1442,7 @@ export function popRenderLanes(fiber: Fiber) {
1442 popFromStack(subtreeRenderLanesCursor, fiber);
1443 }
1444
1457 -function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1445 +function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1446 root.finishedWork = null;
1447 root.finishedLanes = NoLanes;
1448
@@ -1480,7 +1468,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1468 }
1469 }
1470 workInProgressRoot = root;
1483 - workInProgress = createWorkInProgress(root.current, null);
1471 + const rootWorkInProgress = createWorkInProgress(root.current, null);
1472 + workInProgress = rootWorkInProgress;
1473 workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
1474 workInProgressRootExitStatus = RootInProgress;
1475 workInProgressRootFatalError = null;
@@ -1496,6 +1485,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1485 if (__DEV__) {
1486 ReactStrictModeWarnings.discardPendingWarnings();
1487 }
1488 +
1489 + return rootWorkInProgress;
1490 }
1491
1492 function handleError(root, thrownValue): void {
packages/react-reconciler/src/ReactInternalTypes.js
-2
@@ -213,8 +213,6 @@ type BaseFiberRootProperties = {|
213 // Top context object, used by renderSubtreeIntoContainer
214 context: Object | null,
215 pendingContext: Object | null,
216 - // Determines if we should attempt to hydrate on the initial mount
217 - +isDehydrated: boolean,
216
217 // Used by useMutableSource hook to avoid tearing during hydration.
218 mutableSourceEagerHydrationData?: Array<