@samitouri / QOS-React / commits / 2e0d86d221

Allow updating dehydrated root at lower priority without forcing client render (#24082)

* Pass children to hydration root constructor I already made this change for the concurrent root API in #23309. This does the same thing for the legacy API. Doesn't change any behavior, but I will use this in the next steps. * Add isRootDehydrated function Currently this does nothing except read a boolean field, but I'm about to change this logic. Since this is accessed by React DOM, too, I put the function in a separate module that can be deep imported. Previously, it was accessing the FiberRoot directly. The reason it's a separate module is to break a circular dependency between React DOM and the reconciler. * 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. * Wrap useMutableSource logic in feature flag

Andrew Clark committed Mar 20, 2022 at 16:18 UTC 2e0d86d22192ff0b13b71b4ad68fea46bf523ef6
27 files changed +571 -302
packages/react-art/src/ReactART.js
-1
@@ -69,7 +69,6 @@ class Surface extends React.Component {
69 this._mountNode = createContainer(
70 this._surface,
71 LegacyRoot,
72 - false,
72 null,
73 false,
74 false,
packages/react-devtools-shared/src/backend/renderer.js
+7 -2
@@ -2704,9 +2704,14 @@ export function attach(
2704 // TODO: relying on this seems a bit fishy.
2705 const wasMounted =
2706 alternate.memoizedState != null &&
2707 - alternate.memoizedState.element != null;
2707 + alternate.memoizedState.element != null &&
2708 + // A dehydrated root is not considered mounted
2709 + alternate.memoizedState.isDehydrated !== true;
2710 const isMounted =
2709 - current.memoizedState != null && current.memoizedState.element != null;
2711 + current.memoizedState != null &&
2712 + current.memoizedState.element != null &&
2713 + // A dehydrated root is not considered mounted
2714 + current.memoizedState.isDehydrated !== true;
2715 if (!wasMounted && isMounted) {
2716 // Mount a new root.
2717 setRootPseudoKey(currentRootID, current);
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-dom/src/__tests__/ReactDOMRoot-test.js
+9
@@ -253,6 +253,15 @@ describe('ReactDOMRoot', () => {
253 );
254 });
255
256 + it('callback passed to legacy hydrate() API', () => {
257 + container.innerHTML = '<div>Hi</div>';
258 + ReactDOM.hydrate(<div>Hi</div>, container, () => {
259 + Scheduler.unstable_yieldValue('callback');
260 + });
261 + expect(container.textContent).toEqual('Hi');
262 + expect(Scheduler).toHaveYielded(['callback']);
263 + });
264 +
265 it('warns when unmounting with legacy API (no previous content)', () => {
266 const root = ReactDOMClient.createRoot(container);
267 root.render(<div>Hi</div>);
packages/react-dom/src/client/ReactDOMLegacy.js
+79 -40
@@ -27,6 +27,7 @@ import {
27
28 import {
29 createContainer,
30 + createHydrationContainer,
31 findHostInstanceWithNoPortals,
32 updateContainer,
33 flushSync,
@@ -109,34 +110,81 @@ function noopOnRecoverableError() {
110
111 function legacyCreateRootFromDOMContainer(
112 container: Container,
112 - forceHydrate: boolean,
113 + initialChildren: ReactNodeList,
114 + parentComponent: ?React$Component<any, any>,
115 + callback: ?Function,
116 + isHydrationContainer: boolean,
117 ): FiberRoot {
114 - // First clear any existing content.
115 - if (!forceHydrate) {
118 + if (isHydrationContainer) {
119 + if (typeof callback === 'function') {
120 + const originalCallback = callback;
121 + callback = function() {
122 + const instance = getPublicRootInstance(root);
123 + originalCallback.call(instance);
124 + };
125 + }
126 +
127 + const root = createHydrationContainer(
128 + initialChildren,
129 + callback,
130 + container,
131 + LegacyRoot,
132 + null, // hydrationCallbacks
133 + false, // isStrictMode
134 + false, // concurrentUpdatesByDefaultOverride,
135 + '', // identifierPrefix
136 + noopOnRecoverableError,
137 + // TODO(luna) Support hydration later
138 + null,
139 + );
140 + container._reactRootContainer = root;
141 + markContainerAsRoot(root.current, container);
142 +
143 + const rootContainerElement =
144 + container.nodeType === COMMENT_NODE ? container.parentNode : container;
145 + listenToAllSupportedEvents(rootContainerElement);
146 +
147 + flushSync();
148 + return root;
149 + } else {
150 + // First clear any existing content.
151 let rootSibling;
152 while ((rootSibling = container.lastChild)) {
153 container.removeChild(rootSibling);
154 }
120 - }
155
122 - const root = createContainer(
123 - container,
124 - LegacyRoot,
125 - forceHydrate,
126 - null, // hydrationCallbacks
127 - false, // isStrictMode
128 - false, // concurrentUpdatesByDefaultOverride,
129 - '', // identifierPrefix
130 - noopOnRecoverableError, // onRecoverableError
131 - null, // transitionCallbacks
132 - );
133 - markContainerAsRoot(root.current, container);
156 + if (typeof callback === 'function') {
157 + const originalCallback = callback;
158 + callback = function() {
159 + const instance = getPublicRootInstance(root);
160 + originalCallback.call(instance);
161 + };
162 + }
163 +
164 + const root = createContainer(
165 + container,
166 + LegacyRoot,
167 + null, // hydrationCallbacks
168 + false, // isStrictMode
169 + false, // concurrentUpdatesByDefaultOverride,
170 + '', // identifierPrefix
171 + noopOnRecoverableError, // onRecoverableError
172 + null, // transitionCallbacks
173 + );
174 + container._reactRootContainer = root;
175 + markContainerAsRoot(root.current, container);
176 +
177 + const rootContainerElement =
178 + container.nodeType === COMMENT_NODE ? container.parentNode : container;
179 + listenToAllSupportedEvents(rootContainerElement);
180
135 - const rootContainerElement =
136 - container.nodeType === COMMENT_NODE ? container.parentNode : container;
137 - listenToAllSupportedEvents(rootContainerElement);
181 + // Initial mount should not be batched.
182 + flushSync(() => {
183 + updateContainer(initialChildren, root, parentComponent, callback);
184 + });
185
139 - return root;
186 + return root;
187 + }
188 }
189
190 function warnOnInvalidCallback(callback: mixed, callerName: string): void {
@@ -164,39 +212,30 @@ function legacyRenderSubtreeIntoContainer(
212 warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
213 }
214
167 - let root = container._reactRootContainer;
168 - let fiberRoot: FiberRoot;
169 - if (!root) {
215 + const maybeRoot = container._reactRootContainer;
216 + let root: FiberRoot;
217 + if (!maybeRoot) {
218 // Initial mount
171 - root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
219 + root = legacyCreateRootFromDOMContainer(
220 container,
221 + children,
222 + parentComponent,
223 + callback,
224 forceHydrate,
225 );
175 - fiberRoot = root;
176 - if (typeof callback === 'function') {
177 - const originalCallback = callback;
178 - callback = function() {
179 - const instance = getPublicRootInstance(fiberRoot);
180 - originalCallback.call(instance);
181 - };
182 - }
183 - // Initial mount should not be batched.
184 - flushSync(() => {
185 - updateContainer(children, fiberRoot, parentComponent, callback);
186 - });
226 } else {
188 - fiberRoot = root;
227 + root = maybeRoot;
228 if (typeof callback === 'function') {
229 const originalCallback = callback;
230 callback = function() {
192 - const instance = getPublicRootInstance(fiberRoot);
231 + const instance = getPublicRootInstance(root);
232 originalCallback.call(instance);
233 };
234 }
235 // Update
197 - updateContainer(children, fiberRoot, parentComponent, callback);
236 + updateContainer(children, root, parentComponent, callback);
237 }
199 - return getPublicRootInstance(fiberRoot);
238 + return getPublicRootInstance(root);
239 }
240
241 export function findDOMNode(
packages/react-dom/src/client/ReactDOMRoot.js
+1 -1
@@ -224,7 +224,6 @@ export function createRoot(
224 const root = createContainer(
225 container,
226 ConcurrentRoot,
227 - false,
227 null,
228 isStrictMode,
229 concurrentUpdatesByDefaultOverride,
@@ -303,6 +302,7 @@ export function hydrateRoot(
302
303 const root = createHydrationContainer(
304 initialChildren,
305 + null,
306 container,
307 ConcurrentRoot,
308 hydrationCallbacks,
packages/react-dom/src/events/ReactDOMEventListener.js
+2 -1
@@ -53,6 +53,7 @@ import {
53 setCurrentUpdatePriority,
54 } from 'react-reconciler/src/ReactEventPriorities';
55 import ReactSharedInternals from 'shared/ReactSharedInternals';
56 +import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration';
57
58 const {ReactCurrentBatchConfig} = ReactSharedInternals;
59
@@ -386,7 +387,7 @@ export function findInstanceBlockingEvent(
387 targetInst = null;
388 } else if (tag === HostRoot) {
389 const root: FiberRoot = nearestMounted.stateNode;
389 - if (root.isDehydrated) {
390 + if (isRootDehydrated(root)) {
391 // If this happens during a replay something went wrong and it might block
392 // the whole system.
393 return getContainerFromFiber(nearestMounted);
packages/react-dom/src/events/ReactDOMEventReplaying.js
+2 -1
@@ -39,6 +39,7 @@ import {
39 } from '../client/ReactDOMComponentTree';
40 import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
41 import {isHigherEventPriority} from 'react-reconciler/src/ReactEventPriorities';
42 +import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration';
43
44 let _attemptSynchronousHydration: (fiber: Object) => void;
45
@@ -414,7 +415,7 @@ function attemptExplicitHydrationTarget(
415 }
416 } else if (tag === HostRoot) {
417 const root: FiberRoot = nearestMounted.stateNode;
417 - if (root.isDehydrated) {
418 + if (isRootDehydrated(root)) {
419 queuedTarget.blockedOn = getContainerFromFiber(nearestMounted);
420 // We don't currently have a way to increase the priority of
421 // a root other than sync.
packages/react-native-renderer/src/ReactFabric.js
-1
@@ -215,7 +215,6 @@ function render(
215 root = createContainer(
216 containerTag,
217 concurrentRoot ? ConcurrentRoot : LegacyRoot,
218 - false,
218 null,
219 false,
220 null,
packages/react-native-renderer/src/ReactNativeRenderer.js
-1
@@ -211,7 +211,6 @@ function render(
211 root = createContainer(
212 containerTag,
213 LegacyRoot,
214 - false,
214 null,
215 false,
216 null,
packages/react-noop-renderer/src/createReactNoop.js
-3
@@ -974,7 +974,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
974 root = NoopRenderer.createContainer(
975 container,
976 tag,
977 - false,
977 null,
978 null,
979 false,
@@ -996,7 +995,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
995 const fiberRoot = NoopRenderer.createContainer(
996 container,
997 ConcurrentRoot,
999 - false,
998 null,
999 null,
1000 false,
@@ -1029,7 +1027,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
1027 const fiberRoot = NoopRenderer.createContainer(
1028 container,
1029 LegacyRoot,
1032 - false,
1030 null,
1031 null,
1032 false,
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+114 -50
@@ -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,9 +33,11 @@ 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,
40 + enableUseMutableSource,
41 } from 'shared/ReactFeatureFlags';
42
43 import checkPropTypes from 'shared/checkPropTypes';
@@ -1311,14 +1317,9 @@ function pushHostRootContext(workInProgress) {
1317
1318 function updateHostRoot(current, workInProgress, renderLanes) {
1319 pushHostRootContext(workInProgress);
1314 - const updateQueue = workInProgress.updateQueue;
1320
1316 - if (current === null || updateQueue === null) {
1317 - throw new Error(
1318 - 'If the root does not have an updateQueue, we should have already ' +
1319 - 'bailed out. This error is likely caused by a bug in React. Please ' +
1320 - 'file an issue.',
1321 - );
1321 + if (current === null) {
1322 + throw new Error('Should have a current fiber. This is a bug in React.');
1323 }
1324
1325 const nextProps = workInProgress.pendingProps;
@@ -1326,8 +1327,8 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1327 const prevChildren = prevState.element;
1328 cloneUpdateQueue(current, workInProgress);
1329 processUpdateQueue(workInProgress, nextProps, null, renderLanes);
1329 - const nextState = workInProgress.memoizedState;
1330
1331 + const nextState: RootState = workInProgress.memoizedState;
1332 const root: FiberRoot = workInProgress.stateNode;
1333
1334 if (enableCache) {
@@ -1341,64 +1342,127 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1342 }
1343
1344 if (enableTransitionTracing) {
1345 + // FIXME: Slipped past code review. This is not a safe mutation:
1346 + // workInProgress.memoizedState is a shared object. Need to fix before
1347 + // rolling out the Transition Tracing experiment.
1348 workInProgress.memoizedState.transitions = getWorkInProgressTransitions();
1349 }
1350
1351 // Caution: React DevTools currently depends on this property
1352 // being called "element".
1353 const nextChildren = nextState.element;
1350 - if (nextChildren === prevChildren) {
1351 - resetHydrationState();
1352 - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1353 - }
1354 - if (root.isDehydrated && enterHydrationState(workInProgress)) {
1355 - // If we don't have any current children this might be the first pass.
1356 - // We always try to hydrate. If this isn't a hydration pass there won't
1357 - // be any children to hydrate which is effectively the same thing as
1358 - // not hydrating.
1359 -
1360 - if (supportsHydration) {
1361 - const mutableSourceEagerHydrationData =
1362 - root.mutableSourceEagerHydrationData;
1363 - if (mutableSourceEagerHydrationData != null) {
1364 - for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1365 - const mutableSource = ((mutableSourceEagerHydrationData[
1366 - i
1367 - ]: any): MutableSource<any>);
1368 - const version = mutableSourceEagerHydrationData[i + 1];
1369 - setWorkInProgressVersion(mutableSource, version);
1354 + if (supportsHydration && prevState.isDehydrated) {
1355 + // This is a hydration root whose shell has not yet hydrated. We should
1356 + // attempt to hydrate.
1357 +
1358 + // Flip isDehydrated to false to indicate that when this render
1359 + // finishes, the root will no longer be dehydrated.
1360 + const overrideState: RootState = {
1361 + element: nextChildren,
1362 + isDehydrated: false,
1363 + cache: nextState.cache,
1364 + transitions: nextState.transitions,
1365 + };
1366 + const updateQueue: UpdateQueue<RootState> = (workInProgress.updateQueue: any);
1367 + // `baseState` can always be the last state because the root doesn't
1368 + // have reducer functions so it doesn't need rebasing.
1369 + updateQueue.baseState = overrideState;
1370 + workInProgress.memoizedState = overrideState;
1371 +
1372 + if (workInProgress.flags & ForceClientRender) {
1373 + // Something errored during a previous attempt to hydrate the shell, so we
1374 + // forced a client render.
1375 + const recoverableError = new Error(
1376 + 'There was an error while hydrating. Because the error happened outside ' +
1377 + 'of a Suspense boundary, the entire root will switch to ' +
1378 + 'client rendering.',
1379 + );
1380 + return mountHostRootWithoutHydrating(
1381 + current,
1382 + workInProgress,
1383 + nextChildren,
1384 + renderLanes,
1385 + recoverableError,
1386 + );
1387 + } else if (nextChildren !== prevChildren) {
1388 + const recoverableError = new Error(
1389 + 'This root received an early update, before anything was able ' +
1390 + 'hydrate. Switched the entire root to client rendering.',
1391 + );
1392 + return mountHostRootWithoutHydrating(
1393 + current,
1394 + workInProgress,
1395 + nextChildren,
1396 + renderLanes,
1397 + recoverableError,
1398 + );
1399 + } else {
1400 + // The outermost shell has not hydrated yet. Start hydrating.
1401 + enterHydrationState(workInProgress);
1402 + if (enableUseMutableSource && supportsHydration) {
1403 + const mutableSourceEagerHydrationData =
1404 + root.mutableSourceEagerHydrationData;
1405 + if (mutableSourceEagerHydrationData != null) {
1406 + for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1407 + const mutableSource = ((mutableSourceEagerHydrationData[
1408 + i
1409 + ]: any): MutableSource<any>);
1410 + const version = mutableSourceEagerHydrationData[i + 1];
1411 + setWorkInProgressVersion(mutableSource, version);
1412 + }
1413 }
1414 }
1372 - }
1415
1374 - const child = mountChildFibers(
1375 - workInProgress,
1376 - null,
1377 - nextChildren,
1378 - renderLanes,
1379 - );
1380 - workInProgress.child = child;
1416 + const child = mountChildFibers(
1417 + workInProgress,
1418 + null,
1419 + nextChildren,
1420 + renderLanes,
1421 + );
1422 + workInProgress.child = child;
1423
1382 - let node = child;
1383 - while (node) {
1384 - // Mark each child as hydrating. This is a fast path to know whether this
1385 - // tree is part of a hydrating tree. This is used to determine if a child
1386 - // node has fully mounted yet, and for scheduling event replaying.
1387 - // Conceptually this is similar to Placement in that a new subtree is
1388 - // inserted into the React tree here. It just happens to not need DOM
1389 - // mutations because it already exists.
1390 - node.flags = (node.flags & ~Placement) | Hydrating;
1391 - node = node.sibling;
1424 + let node = child;
1425 + while (node) {
1426 + // Mark each child as hydrating. This is a fast path to know whether this
1427 + // tree is part of a hydrating tree. This is used to determine if a child
1428 + // node has fully mounted yet, and for scheduling event replaying.
1429 + // Conceptually this is similar to Placement in that a new subtree is
1430 + // inserted into the React tree here. It just happens to not need DOM
1431 + // mutations because it already exists.
1432 + node.flags = (node.flags & ~Placement) | Hydrating;
1433 + node = node.sibling;
1434 + }
1435 }
1436 } else {
1394 - // Otherwise reset hydration state in case we aborted and resumed another
1395 - // root.
1396 - reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1437 + // Root is not dehydrated. Either this is a client-only root, or it
1438 + // already hydrated.
1439 resetHydrationState();
1440 + if (nextChildren === prevChildren) {
1441 + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1442 + }
1443 + reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1444 }
1445 return workInProgress.child;
1446 }
1447
1448 +function mountHostRootWithoutHydrating(
1449 + current: Fiber,
1450 + workInProgress: Fiber,
1451 + nextChildren: ReactNodeList,
1452 + renderLanes: Lanes,
1453 + recoverableError: Error,
1454 +) {
1455 + // Revert to client rendering.
1456 + resetHydrationState();
1457 +
1458 + queueHydrationError(recoverableError);
1459 +
1460 + workInProgress.flags |= ForceClientRender;
1461 +
1462 + reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1463 + return workInProgress.child;
1464 +}
1465 +
1466 function updateHostComponent(
1467 current: Fiber | null,
1468 workInProgress: Fiber,
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+114 -50
@@ -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,9 +33,11 @@ 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,
40 + enableUseMutableSource,
41 } from 'shared/ReactFeatureFlags';
42
43 import checkPropTypes from 'shared/checkPropTypes';
@@ -1311,14 +1317,9 @@ function pushHostRootContext(workInProgress) {
1317
1318 function updateHostRoot(current, workInProgress, renderLanes) {
1319 pushHostRootContext(workInProgress);
1314 - const updateQueue = workInProgress.updateQueue;
1320
1316 - if (current === null || updateQueue === null) {
1317 - throw new Error(
1318 - 'If the root does not have an updateQueue, we should have already ' +
1319 - 'bailed out. This error is likely caused by a bug in React. Please ' +
1320 - 'file an issue.',
1321 - );
1321 + if (current === null) {
1322 + throw new Error('Should have a current fiber. This is a bug in React.');
1323 }
1324
1325 const nextProps = workInProgress.pendingProps;
@@ -1326,8 +1327,8 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1327 const prevChildren = prevState.element;
1328 cloneUpdateQueue(current, workInProgress);
1329 processUpdateQueue(workInProgress, nextProps, null, renderLanes);
1329 - const nextState = workInProgress.memoizedState;
1330
1331 + const nextState: RootState = workInProgress.memoizedState;
1332 const root: FiberRoot = workInProgress.stateNode;
1333
1334 if (enableCache) {
@@ -1341,64 +1342,127 @@ function updateHostRoot(current, workInProgress, renderLanes) {
1342 }
1343
1344 if (enableTransitionTracing) {
1345 + // FIXME: Slipped past code review. This is not a safe mutation:
1346 + // workInProgress.memoizedState is a shared object. Need to fix before
1347 + // rolling out the Transition Tracing experiment.
1348 workInProgress.memoizedState.transitions = getWorkInProgressTransitions();
1349 }
1350
1351 // Caution: React DevTools currently depends on this property
1352 // being called "element".
1353 const nextChildren = nextState.element;
1350 - if (nextChildren === prevChildren) {
1351 - resetHydrationState();
1352 - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1353 - }
1354 - if (root.isDehydrated && enterHydrationState(workInProgress)) {
1355 - // If we don't have any current children this might be the first pass.
1356 - // We always try to hydrate. If this isn't a hydration pass there won't
1357 - // be any children to hydrate which is effectively the same thing as
1358 - // not hydrating.
1359 -
1360 - if (supportsHydration) {
1361 - const mutableSourceEagerHydrationData =
1362 - root.mutableSourceEagerHydrationData;
1363 - if (mutableSourceEagerHydrationData != null) {
1364 - for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1365 - const mutableSource = ((mutableSourceEagerHydrationData[
1366 - i
1367 - ]: any): MutableSource<any>);
1368 - const version = mutableSourceEagerHydrationData[i + 1];
1369 - setWorkInProgressVersion(mutableSource, version);
1354 + if (supportsHydration && prevState.isDehydrated) {
1355 + // This is a hydration root whose shell has not yet hydrated. We should
1356 + // attempt to hydrate.
1357 +
1358 + // Flip isDehydrated to false to indicate that when this render
1359 + // finishes, the root will no longer be dehydrated.
1360 + const overrideState: RootState = {
1361 + element: nextChildren,
1362 + isDehydrated: false,
1363 + cache: nextState.cache,
1364 + transitions: nextState.transitions,
1365 + };
1366 + const updateQueue: UpdateQueue<RootState> = (workInProgress.updateQueue: any);
1367 + // `baseState` can always be the last state because the root doesn't
1368 + // have reducer functions so it doesn't need rebasing.
1369 + updateQueue.baseState = overrideState;
1370 + workInProgress.memoizedState = overrideState;
1371 +
1372 + if (workInProgress.flags & ForceClientRender) {
1373 + // Something errored during a previous attempt to hydrate the shell, so we
1374 + // forced a client render.
1375 + const recoverableError = new Error(
1376 + 'There was an error while hydrating. Because the error happened outside ' +
1377 + 'of a Suspense boundary, the entire root will switch to ' +
1378 + 'client rendering.',
1379 + );
1380 + return mountHostRootWithoutHydrating(
1381 + current,
1382 + workInProgress,
1383 + nextChildren,
1384 + renderLanes,
1385 + recoverableError,
1386 + );
1387 + } else if (nextChildren !== prevChildren) {
1388 + const recoverableError = new Error(
1389 + 'This root received an early update, before anything was able ' +
1390 + 'hydrate. Switched the entire root to client rendering.',
1391 + );
1392 + return mountHostRootWithoutHydrating(
1393 + current,
1394 + workInProgress,
1395 + nextChildren,
1396 + renderLanes,
1397 + recoverableError,
1398 + );
1399 + } else {
1400 + // The outermost shell has not hydrated yet. Start hydrating.
1401 + enterHydrationState(workInProgress);
1402 + if (enableUseMutableSource && supportsHydration) {
1403 + const mutableSourceEagerHydrationData =
1404 + root.mutableSourceEagerHydrationData;
1405 + if (mutableSourceEagerHydrationData != null) {
1406 + for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1407 + const mutableSource = ((mutableSourceEagerHydrationData[
1408 + i
1409 + ]: any): MutableSource<any>);
1410 + const version = mutableSourceEagerHydrationData[i + 1];
1411 + setWorkInProgressVersion(mutableSource, version);
1412 + }
1413 }
1414 }
1372 - }
1415
1374 - const child = mountChildFibers(
1375 - workInProgress,
1376 - null,
1377 - nextChildren,
1378 - renderLanes,
1379 - );
1380 - workInProgress.child = child;
1416 + const child = mountChildFibers(
1417 + workInProgress,
1418 + null,
1419 + nextChildren,
1420 + renderLanes,
1421 + );
1422 + workInProgress.child = child;
1423
1382 - let node = child;
1383 - while (node) {
1384 - // Mark each child as hydrating. This is a fast path to know whether this
1385 - // tree is part of a hydrating tree. This is used to determine if a child
1386 - // node has fully mounted yet, and for scheduling event replaying.
1387 - // Conceptually this is similar to Placement in that a new subtree is
1388 - // inserted into the React tree here. It just happens to not need DOM
1389 - // mutations because it already exists.
1390 - node.flags = (node.flags & ~Placement) | Hydrating;
1391 - node = node.sibling;
1424 + let node = child;
1425 + while (node) {
1426 + // Mark each child as hydrating. This is a fast path to know whether this
1427 + // tree is part of a hydrating tree. This is used to determine if a child
1428 + // node has fully mounted yet, and for scheduling event replaying.
1429 + // Conceptually this is similar to Placement in that a new subtree is
1430 + // inserted into the React tree here. It just happens to not need DOM
1431 + // mutations because it already exists.
1432 + node.flags = (node.flags & ~Placement) | Hydrating;
1433 + node = node.sibling;
1434 + }
1435 }
1436 } else {
1394 - // Otherwise reset hydration state in case we aborted and resumed another
1395 - // root.
1396 - reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1437 + // Root is not dehydrated. Either this is a client-only root, or it
1438 + // already hydrated.
1439 resetHydrationState();
1440 + if (nextChildren === prevChildren) {
1441 + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
1442 + }
1443 + reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1444 }
1445 return workInProgress.child;
1446 }
1447
1448 +function mountHostRootWithoutHydrating(
1449 + current: Fiber,
1450 + workInProgress: Fiber,
1451 + nextChildren: ReactNodeList,
1452 + renderLanes: Lanes,
1453 + recoverableError: Error,
1454 +) {
1455 + // Revert to client rendering.
1456 + resetHydrationState();
1457 +
1458 + queueHydrationError(recoverableError);
1459 +
1460 + workInProgress.flags |= ForceClientRender;
1461 +
1462 + reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1463 + return workInProgress.child;
1464 +}
1465 +
1466 function updateHostComponent(
1467 current: Fiber | null,
1468 workInProgress: Fiber,
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+13 -10
@@ -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,
@@ -1869,11 +1870,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1870 }
1871 case HostRoot: {
1872 if (supportsHydration) {
1872 - const root: FiberRoot = finishedWork.stateNode;
1873 - if (root.isDehydrated) {
1874 - // We've just hydrated. No need to hydrate again.
1875 - root.isDehydrated = false;
1876 - commitHydratedContainer(root.containerInfo);
1873 + if (current !== null) {
1874 + const prevRootState: RootState = current.memoizedState;
1875 + if (prevRootState.isDehydrated) {
1876 + const root: FiberRoot = finishedWork.stateNode;
1877 + commitHydratedContainer(root.containerInfo);
1878 + }
1879 }
1880 }
1881 break;
@@ -1977,11 +1979,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1979 }
1980 case HostRoot: {
1981 if (supportsHydration) {
1980 - const root: FiberRoot = finishedWork.stateNode;
1981 - if (root.isDehydrated) {
1982 - // We've just hydrated. No need to hydrate again.
1983 - root.isDehydrated = false;
1984 - commitHydratedContainer(root.containerInfo);
1982 + if (current !== null) {
1983 + const prevRootState: RootState = current.memoizedState;
1984 + if (prevRootState.isDehydrated) {
1985 + const root: FiberRoot = finishedWork.stateNode;
1986 + commitHydratedContainer(root.containerInfo);
1987 + }
1988 }
1989 }
1990 return;
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+13 -10
@@ -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,
@@ -1869,11 +1870,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1870 }
1871 case HostRoot: {
1872 if (supportsHydration) {
1872 - const root: FiberRoot = finishedWork.stateNode;
1873 - if (root.isDehydrated) {
1874 - // We've just hydrated. No need to hydrate again.
1875 - root.isDehydrated = false;
1876 - commitHydratedContainer(root.containerInfo);
1873 + if (current !== null) {
1874 + const prevRootState: RootState = current.memoizedState;
1875 + if (prevRootState.isDehydrated) {
1876 + const root: FiberRoot = finishedWork.stateNode;
1877 + commitHydratedContainer(root.containerInfo);
1878 + }
1879 }
1880 }
1881 break;
@@ -1977,11 +1979,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1979 }
1980 case HostRoot: {
1981 if (supportsHydration) {
1980 - const root: FiberRoot = finishedWork.stateNode;
1981 - if (root.isDehydrated) {
1982 - // We've just hydrated. No need to hydrate again.
1983 - root.isDehydrated = false;
1984 - commitHydratedContainer(root.containerInfo);
1982 + if (current !== null) {
1983 + const prevRootState: RootState = current.memoizedState;
1984 + if (prevRootState.isDehydrated) {
1985 + const root: FiberRoot = finishedWork.stateNode;
1986 + commitHydratedContainer(root.containerInfo);
1987 + }
1988 }
1989 }
1990 return;
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+24 -6
@@ -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,
@@ -890,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);
893 - } else if (!fiberRoot.isDehydrated) {
894 - // Schedule an effect to clear this container at the start of the next commit.
895 - // This handles the case of React rendering into a container with previous children.
896 - // It's also safe to do for updates too, because current.child would only be null
897 - // if the previous render was null (so the container would already be empty).
898 - 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 -6
@@ -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,
@@ -890,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);
893 - } else if (!fiberRoot.isDehydrated) {
894 - // Schedule an effect to clear this container at the start of the next commit.
895 - // This handles the case of React rendering into a container with previous children.
896 - // It's also safe to do for updates too, because current.child would only be null
897 - // if the previous render was null (so the container would already be empty).
898 - 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
+13 -7
@@ -48,6 +48,7 @@ import {
48 isContextProvider as isLegacyContextProvider,
49 } from './ReactFiberContext.new';
50 import {createFiberRoot} from './ReactFiberRoot.new';
51 +import {isRootDehydrated} from './ReactFiberShellHydration';
52 import {
53 injectInternals,
54 markRenderScheduled,
@@ -245,9 +246,6 @@ function findHostInstanceWithWarning(
246 export function createContainer(
247 containerInfo: Container,
248 tag: RootTag,
248 - // TODO: We can remove hydration-specific stuff from createContainer once
249 - // we delete legacy mode. The new root API uses createHydrationContainer.
250 - hydrate: boolean,
249 hydrationCallbacks: null | SuspenseHydrationCallbacks,
250 isStrictMode: boolean,
251 concurrentUpdatesByDefaultOverride: null | boolean,
@@ -255,10 +253,13 @@ export function createContainer(
253 onRecoverableError: (error: mixed) => void,
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,
@@ -270,6 +271,8 @@ export function createContainer(
271
272 export function createHydrationContainer(
273 initialChildren: ReactNodeList,
274 + // TODO: Remove `callback` when we delete legacy mode.
275 + callback: ?Function,
276 containerInfo: Container,
277 tag: RootTag,
278 hydrationCallbacks: null | SuspenseHydrationCallbacks,
@@ -284,6 +287,7 @@ export function createHydrationContainer(
287 containerInfo,
288 tag,
289 hydrate,
290 + initialChildren,
291 hydrationCallbacks,
292 isStrictMode,
293 concurrentUpdatesByDefaultOverride,
@@ -298,13 +302,15 @@ export function createHydrationContainer(
302 // Schedule the initial render. In a hydration root, this is different from
303 // a regular update because the initial render must match was was rendered
304 // on the server.
305 + // NOTE: This update intentionally doesn't have a payload. We're only using
306 + // the update to schedule work on the root fiber (and, for legacy roots, to
307 + // enqueue the callback if one is provided).
308 const current = root.current;
309 const eventTime = requestEventTime();
310 const lane = requestUpdateLane(current);
311 const update = createUpdate(eventTime, lane);
305 - // Caution: React DevTools currently depends on this property
306 - // being called "element".
307 - update.payload = {element: initialChildren};
312 + update.callback =
313 + callback !== undefined && callback !== null ? callback : null;
314 enqueueUpdate(current, update, lane);
315 scheduleInitialHydrationOnRoot(root, lane, eventTime);
316
@@ -409,7 +415,7 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
415 switch (fiber.tag) {
416 case HostRoot:
417 const root: FiberRoot = fiber.stateNode;
412 - if (root.isDehydrated) {
418 + if (isRootDehydrated(root)) {
419 // Flush the first scheduled "update".
420 const lanes = getHighestPriorityPendingLanes(root);
421 flushRoot(root, lanes);
packages/react-reconciler/src/ReactFiberReconciler.old.js
+13 -7
@@ -48,6 +48,7 @@ import {
48 isContextProvider as isLegacyContextProvider,
49 } from './ReactFiberContext.old';
50 import {createFiberRoot} from './ReactFiberRoot.old';
51 +import {isRootDehydrated} from './ReactFiberShellHydration';
52 import {
53 injectInternals,
54 markRenderScheduled,
@@ -245,9 +246,6 @@ function findHostInstanceWithWarning(
246 export function createContainer(
247 containerInfo: Container,
248 tag: RootTag,
248 - // TODO: We can remove hydration-specific stuff from createContainer once
249 - // we delete legacy mode. The new root API uses createHydrationContainer.
250 - hydrate: boolean,
249 hydrationCallbacks: null | SuspenseHydrationCallbacks,
250 isStrictMode: boolean,
251 concurrentUpdatesByDefaultOverride: null | boolean,
@@ -255,10 +253,13 @@ export function createContainer(
253 onRecoverableError: (error: mixed) => void,
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,
@@ -270,6 +271,8 @@ export function createContainer(
271
272 export function createHydrationContainer(
273 initialChildren: ReactNodeList,
274 + // TODO: Remove `callback` when we delete legacy mode.
275 + callback: ?Function,
276 containerInfo: Container,
277 tag: RootTag,
278 hydrationCallbacks: null | SuspenseHydrationCallbacks,
@@ -284,6 +287,7 @@ export function createHydrationContainer(
287 containerInfo,
288 tag,
289 hydrate,
290 + initialChildren,
291 hydrationCallbacks,
292 isStrictMode,
293 concurrentUpdatesByDefaultOverride,
@@ -298,13 +302,15 @@ export function createHydrationContainer(
302 // Schedule the initial render. In a hydration root, this is different from
303 // a regular update because the initial render must match was was rendered
304 // on the server.
305 + // NOTE: This update intentionally doesn't have a payload. We're only using
306 + // the update to schedule work on the root fiber (and, for legacy roots, to
307 + // enqueue the callback if one is provided).
308 const current = root.current;
309 const eventTime = requestEventTime();
310 const lane = requestUpdateLane(current);
311 const update = createUpdate(eventTime, lane);
305 - // Caution: React DevTools currently depends on this property
306 - // being called "element".
307 - update.payload = {element: initialChildren};
312 + update.callback =
313 + callback !== undefined && callback !== null ? callback : null;
314 enqueueUpdate(current, update, lane);
315 scheduleInitialHydrationOnRoot(root, lane, eventTime);
316
@@ -409,7 +415,7 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
415 switch (fiber.tag) {
416 case HostRoot:
417 const root: FiberRoot = fiber.stateNode;
412 - if (root.isDehydrated) {
418 + if (isRootDehydrated(root)) {
419 // Flush the first scheduled "update".
420 const lanes = getHighestPriorityPendingLanes(root);
421 flushRoot(root, lanes);
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 new
+19
@@ -0,0 +1,19 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
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) {
17 + const currentState: RootState = root.current.memoizedState;
18 + return currentState.isDehydrated;
19 +}
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+36 -44
@@ -88,6 +88,7 @@ import {
88 createWorkInProgress,
89 assignFiberPropertiesInDEV,
90 } from './ReactFiber.new';
91 +import {isRootDehydrated} from './ReactFiberShellHydration';
92 import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode';
93 import {
94 HostRoot,
@@ -109,6 +110,7 @@ import {
110 StoreConsistency,
111 HostEffectMask,
112 Hydrating,
113 + ForceClientRender,
114 BeforeMutationMask,
115 MutationMask,
116 LayoutMask,
@@ -581,34 +583,7 @@ export function scheduleUpdateOnFiber(
583 }
584 }
585
584 - if (root.isDehydrated && root.tag !== LegacyRoot) {
585 - // This root's shell hasn't hydrated yet. Revert to client rendering.
586 - if (workInProgressRoot === root) {
587 - // If this happened during an interleaved event, interrupt the
588 - // in-progress hydration. Theoretically, we could attempt to force a
589 - // synchronous hydration before switching to client rendering, but the
590 - // most common reason the shell hasn't hydrated yet is because it
591 - // suspended. So it's very likely to suspend again anyway. For
592 - // simplicity, we'll skip that atttempt and go straight to
593 - // client rendering.
594 - //
595 - // Another way to model this would be to give the initial hydration its
596 - // own special lane. However, it may not be worth adding a lane solely
597 - // for this purpose, so we'll wait until we find another use case before
598 - // adding it.
599 - //
600 - // TODO: Consider only interrupting hydration if the priority of the
601 - // update is higher than default.
602 - prepareFreshStack(root, NoLanes);
603 - }
604 - root.isDehydrated = false;
605 - const error = new Error(
606 - 'This root received an early update, before anything was able ' +
607 - 'hydrate. Switched the entire root to client rendering.',
608 - );
609 - const onRecoverableError = root.onRecoverableError;
610 - onRecoverableError(error);
611 - } 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
@@ -1016,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.
1019 - if (root.isDehydrated) {
1020 - root.isDehydrated = false;
994 +
995 + // Before rendering again, save the errors from the previous attempt.
996 + const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
997 +
998 + if (isRootDehydrated(root)) {
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 }
1024 - const error = new Error(
1025 - 'There was an error while hydrating. Because the error happened outside ' +
1026 - 'of a Suspense boundary, the entire root will switch to ' +
1027 - 'client rendering.',
1028 - );
1029 - renderDidError(error);
1015 }
1016
1032 - const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
1017 const exitStatus = renderRootSync(root, errorRetryLanes);
1018 if (exitStatus !== RootErrored) {
1019 // Successfully finished rendering on retry
1036 - if (errorsFromFirstAttempt !== null) {
1037 - // The errors from the failed first attempt have been recovered. Add
1038 - // them to the collection of recoverable errors. We'll log them in the
1039 - // commit phase.
1040 - 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.
@@ -1453,7 +1442,7 @@ export function popRenderLanes(fiber: Fiber) {
1442 popFromStack(subtreeRenderLanesCursor, fiber);
1443 }
1444
1456 -function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1445 +function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1446 root.finishedWork = null;
1447 root.finishedLanes = NoLanes;
1448
@@ -1479,7 +1468,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1468 }
1469 }
1470 workInProgressRoot = root;
1482 - 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;
@@ -1495,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
+36 -44
@@ -88,6 +88,7 @@ import {
88 createWorkInProgress,
89 assignFiberPropertiesInDEV,
90 } from './ReactFiber.old';
91 +import {isRootDehydrated} from './ReactFiberShellHydration';
92 import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode';
93 import {
94 HostRoot,
@@ -109,6 +110,7 @@ import {
110 StoreConsistency,
111 HostEffectMask,
112 Hydrating,
113 + ForceClientRender,
114 BeforeMutationMask,
115 MutationMask,
116 LayoutMask,
@@ -581,34 +583,7 @@ export function scheduleUpdateOnFiber(
583 }
584 }
585
584 - if (root.isDehydrated && root.tag !== LegacyRoot) {
585 - // This root's shell hasn't hydrated yet. Revert to client rendering.
586 - if (workInProgressRoot === root) {
587 - // If this happened during an interleaved event, interrupt the
588 - // in-progress hydration. Theoretically, we could attempt to force a
589 - // synchronous hydration before switching to client rendering, but the
590 - // most common reason the shell hasn't hydrated yet is because it
591 - // suspended. So it's very likely to suspend again anyway. For
592 - // simplicity, we'll skip that atttempt and go straight to
593 - // client rendering.
594 - //
595 - // Another way to model this would be to give the initial hydration its
596 - // own special lane. However, it may not be worth adding a lane solely
597 - // for this purpose, so we'll wait until we find another use case before
598 - // adding it.
599 - //
600 - // TODO: Consider only interrupting hydration if the priority of the
601 - // update is higher than default.
602 - prepareFreshStack(root, NoLanes);
603 - }
604 - root.isDehydrated = false;
605 - const error = new Error(
606 - 'This root received an early update, before anything was able ' +
607 - 'hydrate. Switched the entire root to client rendering.',
608 - );
609 - const onRecoverableError = root.onRecoverableError;
610 - onRecoverableError(error);
611 - } 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
@@ -1016,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.
1019 - if (root.isDehydrated) {
1020 - root.isDehydrated = false;
994 +
995 + // Before rendering again, save the errors from the previous attempt.
996 + const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
997 +
998 + if (isRootDehydrated(root)) {
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 }
1024 - const error = new Error(
1025 - 'There was an error while hydrating. Because the error happened outside ' +
1026 - 'of a Suspense boundary, the entire root will switch to ' +
1027 - 'client rendering.',
1028 - );
1029 - renderDidError(error);
1015 }
1016
1032 - const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
1017 const exitStatus = renderRootSync(root, errorRetryLanes);
1018 if (exitStatus !== RootErrored) {
1019 // Successfully finished rendering on retry
1036 - if (errorsFromFirstAttempt !== null) {
1037 - // The errors from the failed first attempt have been recovered. Add
1038 - // them to the collection of recoverable errors. We'll log them in the
1039 - // commit phase.
1040 - 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.
@@ -1453,7 +1442,7 @@ export function popRenderLanes(fiber: Fiber) {
1442 popFromStack(subtreeRenderLanesCursor, fiber);
1443 }
1444
1456 -function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1445 +function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1446 root.finishedWork = null;
1447 root.finishedLanes = NoLanes;
1448
@@ -1479,7 +1468,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1468 }
1469 }
1470 workInProgressRoot = root;
1482 - 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;
@@ -1495,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<
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
-2
@@ -72,7 +72,6 @@ describe('ReactFiberHostContext', () => {
72 const container = Renderer.createContainer(
73 /* root: */ null,
74 ConcurrentRoot,
75 - false,
75 null,
76 false,
77 '',
@@ -136,7 +135,6 @@ describe('ReactFiberHostContext', () => {
135 const container = Renderer.createContainer(
136 rootContext,
137 ConcurrentRoot,
139 - false,
138 null,
139 false,
140 '',
packages/react-test-renderer/src/ReactTestRenderer.js
-1
@@ -473,7 +473,6 @@ function create(element: React$Element<any>, options: TestRendererOptions) {
473 let root: FiberRoot | null = createContainer(
474 container,
475 isConcurrent ? ConcurrentRoot : LegacyRoot,
476 - false,
476 null,
477 isStrictMode,
478 concurrentUpdatesByDefault,