diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
index f3ff64daee..3584715f91 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
@@ -9,6 +9,7 @@
let JSDOM;
let React;
+let startTransition;
let ReactDOMClient;
let Scheduler;
let clientAct;
@@ -33,6 +34,8 @@ describe('ReactDOMFizzShellHydration', () => {
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
+ startTransition = React.startTransition;
+
textCache = new Map();
// Test Environment
@@ -214,7 +217,36 @@ describe('ReactDOMFizzShellHydration', () => {
expect(container.textContent).toBe('Shell');
});
- test('updating the root before the shell hydrates forces a client render', async () => {
+ test(
+ 'updating the root at lower priority than initial hydration does not ' +
+ 'force a client render',
+ async () => {
+ function App() {
+ return ;
+ }
+
+ // Server render
+ await resolveText('Initial');
+ await serverAct(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+ expect(Scheduler).toHaveYielded(['Initial']);
+
+ await clientAct(async () => {
+ const root = ReactDOMClient.hydrateRoot(container, );
+ // This has lower priority than the initial hydration, so the update
+ // won't be processed until after hydration finishes.
+ startTransition(() => {
+ root.render();
+ });
+ });
+ expect(Scheduler).toHaveYielded(['Initial', 'Updated']);
+ expect(container.textContent).toBe('Updated');
+ },
+ );
+
+ test('updating the root while the shell is suspended forces a client render', async () => {
function App() {
return ;
}
@@ -245,9 +277,9 @@ describe('ReactDOMFizzShellHydration', () => {
root.render();
});
expect(Scheduler).toHaveYielded([
+ 'New screen',
'This root received an early update, before anything was able ' +
'hydrate. Switched the entire root to client rendering.',
- 'New screen',
]);
expect(container.textContent).toBe('New screen');
});
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 4feff7d260..7dc29bd2b0 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -7,7 +7,11 @@
* @flow
*/
-import type {ReactProviderType, ReactContext} from 'shared/ReactTypes';
+import type {
+ ReactProviderType,
+ ReactContext,
+ ReactNodeList,
+} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {TypeOfMode} from './ReactTypeOfMode';
@@ -29,6 +33,7 @@ import type {
SpawnedCachePool,
} from './ReactFiberCacheComponent.new';
import type {UpdateQueue} from './ReactUpdateQueue.new';
+import type {RootState} from './ReactFiberRoot.new';
import {
enableSuspenseAvoidThisFallback,
enableCPUSuspense,
@@ -223,7 +228,6 @@ import {
createOffscreenHostContainerFiber,
isSimpleFunctionComponent,
} from './ReactFiber.new';
-import {isRootDehydrated} from './ReactFiberShellHydration';
import {
retryDehydratedSuspenseBoundary,
scheduleUpdateOnFiber,
@@ -1312,7 +1316,7 @@ function pushHostRootContext(workInProgress) {
function updateHostRoot(current, workInProgress, renderLanes) {
pushHostRootContext(workInProgress);
- const updateQueue = workInProgress.updateQueue;
+ const updateQueue: UpdateQueue = (workInProgress.updateQueue: any);
if (current === null || updateQueue === null) {
throw new Error(
@@ -1327,7 +1331,7 @@ function updateHostRoot(current, workInProgress, renderLanes) {
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
- const nextState = workInProgress.memoizedState;
+ const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
@@ -1342,64 +1346,130 @@ function updateHostRoot(current, workInProgress, renderLanes) {
}
if (enableTransitionTracing) {
+ // FIXME: Slipped past code review. This is not a safe mutation:
+ // workInProgress.memoizedState is a shared object. Need to fix before
+ // rolling out the Transition Tracing experiment.
workInProgress.memoizedState.transitions = getWorkInProgressTransitions();
}
// Caution: React DevTools currently depends on this property
// being called "element".
const nextChildren = nextState.element;
- if (nextChildren === prevChildren) {
- resetHydrationState();
- return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
- }
- if (isRootDehydrated(root) && enterHydrationState(workInProgress)) {
- // If we don't have any current children this might be the first pass.
- // We always try to hydrate. If this isn't a hydration pass there won't
- // be any children to hydrate which is effectively the same thing as
- // not hydrating.
-
- if (supportsHydration) {
- const mutableSourceEagerHydrationData =
- root.mutableSourceEagerHydrationData;
- if (mutableSourceEagerHydrationData != null) {
- for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
- const mutableSource = ((mutableSourceEagerHydrationData[
- i
- ]: any): MutableSource);
- const version = mutableSourceEagerHydrationData[i + 1];
- setWorkInProgressVersion(mutableSource, version);
+ if (supportsHydration && prevState.isDehydrated) {
+ // This is a hydration root whose shell has not yet hydrated. We should
+ // attempt to hydrate.
+ if (workInProgress.flags & ForceClientRender) {
+ // Something errored during a previous attempt to hydrate the shell, so we
+ // forced a client render.
+ const recoverableError = new Error(
+ 'There was an error while hydrating. Because the error happened outside ' +
+ 'of a Suspense boundary, the entire root will switch to ' +
+ 'client rendering.',
+ );
+ return mountHostRootWithoutHydrating(
+ current,
+ workInProgress,
+ updateQueue,
+ nextState,
+ nextChildren,
+ renderLanes,
+ recoverableError,
+ );
+ } else if (nextChildren !== prevChildren) {
+ const recoverableError = new Error(
+ 'This root received an early update, before anything was able ' +
+ 'hydrate. Switched the entire root to client rendering.',
+ );
+ return mountHostRootWithoutHydrating(
+ current,
+ workInProgress,
+ updateQueue,
+ nextState,
+ nextChildren,
+ renderLanes,
+ recoverableError,
+ );
+ } else {
+ // The outermost shell has not hydrated yet. Start hydrating.
+ enterHydrationState(workInProgress);
+ if (supportsHydration) {
+ const mutableSourceEagerHydrationData =
+ root.mutableSourceEagerHydrationData;
+ if (mutableSourceEagerHydrationData != null) {
+ for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
+ const mutableSource = ((mutableSourceEagerHydrationData[
+ i
+ ]: any): MutableSource);
+ const version = mutableSourceEagerHydrationData[i + 1];
+ setWorkInProgressVersion(mutableSource, version);
+ }
}
}
- }
- const child = mountChildFibers(
- workInProgress,
- null,
- nextChildren,
- renderLanes,
- );
- workInProgress.child = child;
+ const child = mountChildFibers(
+ workInProgress,
+ null,
+ nextChildren,
+ renderLanes,
+ );
+ workInProgress.child = child;
- let node = child;
- while (node) {
- // Mark each child as hydrating. This is a fast path to know whether this
- // tree is part of a hydrating tree. This is used to determine if a child
- // node has fully mounted yet, and for scheduling event replaying.
- // Conceptually this is similar to Placement in that a new subtree is
- // inserted into the React tree here. It just happens to not need DOM
- // mutations because it already exists.
- node.flags = (node.flags & ~Placement) | Hydrating;
- node = node.sibling;
+ let node = child;
+ while (node) {
+ // Mark each child as hydrating. This is a fast path to know whether this
+ // tree is part of a hydrating tree. This is used to determine if a child
+ // node has fully mounted yet, and for scheduling event replaying.
+ // Conceptually this is similar to Placement in that a new subtree is
+ // inserted into the React tree here. It just happens to not need DOM
+ // mutations because it already exists.
+ node.flags = (node.flags & ~Placement) | Hydrating;
+ node = node.sibling;
+ }
}
} else {
- // Otherwise reset hydration state in case we aborted and resumed another
- // root.
- reconcileChildren(current, workInProgress, nextChildren, renderLanes);
+ // Root is not dehydrated. Either this is a client-only root, or it
+ // already hydrated.
resetHydrationState();
+ if (nextChildren === prevChildren) {
+ return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
+ }
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
return workInProgress.child;
}
+function mountHostRootWithoutHydrating(
+ current: Fiber,
+ workInProgress: Fiber,
+ updateQueue: UpdateQueue,
+ nextState: RootState,
+ nextChildren: ReactNodeList,
+ renderLanes: Lanes,
+ recoverableError: Error,
+) {
+ // Revert to client rendering.
+ resetHydrationState();
+
+ queueHydrationError(recoverableError);
+
+ workInProgress.flags |= ForceClientRender;
+
+ // Flip isDehydrated to false to indicate that when this render
+ // finishes, the root will no longer be dehydrated.
+ const overrideState: RootState = {
+ element: nextChildren,
+ isDehydrated: false,
+ cache: nextState.cache,
+ transitions: nextState.transitions,
+ };
+ // `baseState` can always be the last state because the root doesn't
+ // have reducer functions so it doesn't need rebasing.
+ updateQueue.baseState = overrideState;
+ workInProgress.memoizedState = overrideState;
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
+ return workInProgress.child;
+}
+
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
index bdb53d6b36..206773b03b 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -7,7 +7,11 @@
* @flow
*/
-import type {ReactProviderType, ReactContext} from 'shared/ReactTypes';
+import type {
+ ReactProviderType,
+ ReactContext,
+ ReactNodeList,
+} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {TypeOfMode} from './ReactTypeOfMode';
@@ -29,6 +33,7 @@ import type {
SpawnedCachePool,
} from './ReactFiberCacheComponent.old';
import type {UpdateQueue} from './ReactUpdateQueue.old';
+import type {RootState} from './ReactFiberRoot.old';
import {
enableSuspenseAvoidThisFallback,
enableCPUSuspense,
@@ -223,7 +228,6 @@ import {
createOffscreenHostContainerFiber,
isSimpleFunctionComponent,
} from './ReactFiber.old';
-import {isRootDehydrated} from './ReactFiberShellHydration';
import {
retryDehydratedSuspenseBoundary,
scheduleUpdateOnFiber,
@@ -1312,7 +1316,7 @@ function pushHostRootContext(workInProgress) {
function updateHostRoot(current, workInProgress, renderLanes) {
pushHostRootContext(workInProgress);
- const updateQueue = workInProgress.updateQueue;
+ const updateQueue: UpdateQueue = (workInProgress.updateQueue: any);
if (current === null || updateQueue === null) {
throw new Error(
@@ -1327,7 +1331,7 @@ function updateHostRoot(current, workInProgress, renderLanes) {
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
- const nextState = workInProgress.memoizedState;
+ const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
@@ -1342,64 +1346,130 @@ function updateHostRoot(current, workInProgress, renderLanes) {
}
if (enableTransitionTracing) {
+ // FIXME: Slipped past code review. This is not a safe mutation:
+ // workInProgress.memoizedState is a shared object. Need to fix before
+ // rolling out the Transition Tracing experiment.
workInProgress.memoizedState.transitions = getWorkInProgressTransitions();
}
// Caution: React DevTools currently depends on this property
// being called "element".
const nextChildren = nextState.element;
- if (nextChildren === prevChildren) {
- resetHydrationState();
- return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
- }
- if (isRootDehydrated(root) && enterHydrationState(workInProgress)) {
- // If we don't have any current children this might be the first pass.
- // We always try to hydrate. If this isn't a hydration pass there won't
- // be any children to hydrate which is effectively the same thing as
- // not hydrating.
-
- if (supportsHydration) {
- const mutableSourceEagerHydrationData =
- root.mutableSourceEagerHydrationData;
- if (mutableSourceEagerHydrationData != null) {
- for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
- const mutableSource = ((mutableSourceEagerHydrationData[
- i
- ]: any): MutableSource);
- const version = mutableSourceEagerHydrationData[i + 1];
- setWorkInProgressVersion(mutableSource, version);
+ if (supportsHydration && prevState.isDehydrated) {
+ // This is a hydration root whose shell has not yet hydrated. We should
+ // attempt to hydrate.
+ if (workInProgress.flags & ForceClientRender) {
+ // Something errored during a previous attempt to hydrate the shell, so we
+ // forced a client render.
+ const recoverableError = new Error(
+ 'There was an error while hydrating. Because the error happened outside ' +
+ 'of a Suspense boundary, the entire root will switch to ' +
+ 'client rendering.',
+ );
+ return mountHostRootWithoutHydrating(
+ current,
+ workInProgress,
+ updateQueue,
+ nextState,
+ nextChildren,
+ renderLanes,
+ recoverableError,
+ );
+ } else if (nextChildren !== prevChildren) {
+ const recoverableError = new Error(
+ 'This root received an early update, before anything was able ' +
+ 'hydrate. Switched the entire root to client rendering.',
+ );
+ return mountHostRootWithoutHydrating(
+ current,
+ workInProgress,
+ updateQueue,
+ nextState,
+ nextChildren,
+ renderLanes,
+ recoverableError,
+ );
+ } else {
+ // The outermost shell has not hydrated yet. Start hydrating.
+ enterHydrationState(workInProgress);
+ if (supportsHydration) {
+ const mutableSourceEagerHydrationData =
+ root.mutableSourceEagerHydrationData;
+ if (mutableSourceEagerHydrationData != null) {
+ for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
+ const mutableSource = ((mutableSourceEagerHydrationData[
+ i
+ ]: any): MutableSource);
+ const version = mutableSourceEagerHydrationData[i + 1];
+ setWorkInProgressVersion(mutableSource, version);
+ }
}
}
- }
- const child = mountChildFibers(
- workInProgress,
- null,
- nextChildren,
- renderLanes,
- );
- workInProgress.child = child;
+ const child = mountChildFibers(
+ workInProgress,
+ null,
+ nextChildren,
+ renderLanes,
+ );
+ workInProgress.child = child;
- let node = child;
- while (node) {
- // Mark each child as hydrating. This is a fast path to know whether this
- // tree is part of a hydrating tree. This is used to determine if a child
- // node has fully mounted yet, and for scheduling event replaying.
- // Conceptually this is similar to Placement in that a new subtree is
- // inserted into the React tree here. It just happens to not need DOM
- // mutations because it already exists.
- node.flags = (node.flags & ~Placement) | Hydrating;
- node = node.sibling;
+ let node = child;
+ while (node) {
+ // Mark each child as hydrating. This is a fast path to know whether this
+ // tree is part of a hydrating tree. This is used to determine if a child
+ // node has fully mounted yet, and for scheduling event replaying.
+ // Conceptually this is similar to Placement in that a new subtree is
+ // inserted into the React tree here. It just happens to not need DOM
+ // mutations because it already exists.
+ node.flags = (node.flags & ~Placement) | Hydrating;
+ node = node.sibling;
+ }
}
} else {
- // Otherwise reset hydration state in case we aborted and resumed another
- // root.
- reconcileChildren(current, workInProgress, nextChildren, renderLanes);
+ // Root is not dehydrated. Either this is a client-only root, or it
+ // already hydrated.
resetHydrationState();
+ if (nextChildren === prevChildren) {
+ return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
+ }
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
return workInProgress.child;
}
+function mountHostRootWithoutHydrating(
+ current: Fiber,
+ workInProgress: Fiber,
+ updateQueue: UpdateQueue,
+ nextState: RootState,
+ nextChildren: ReactNodeList,
+ renderLanes: Lanes,
+ recoverableError: Error,
+) {
+ // Revert to client rendering.
+ resetHydrationState();
+
+ queueHydrationError(recoverableError);
+
+ workInProgress.flags |= ForceClientRender;
+
+ // Flip isDehydrated to false to indicate that when this render
+ // finishes, the root will no longer be dehydrated.
+ const overrideState: RootState = {
+ element: nextChildren,
+ isDehydrated: false,
+ cache: nextState.cache,
+ transitions: nextState.transitions,
+ };
+ // `baseState` can always be the last state because the root doesn't
+ // have reducer functions so it doesn't need rebasing.
+ updateQueue.baseState = overrideState;
+ workInProgress.memoizedState = overrideState;
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
+ return workInProgress.child;
+}
+
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
index 6f6b59b4ec..776c402a00 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
@@ -25,6 +25,7 @@ import type {Wakeable} from 'shared/ReactTypes';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import type {HookFlags} from './ReactHookEffectTags';
import type {Cache} from './ReactFiberCacheComponent.new';
+import type {RootState} from './ReactFiberRoot.new';
import {
enableCreateEventHandleAPI,
@@ -82,7 +83,6 @@ import {
Visibility,
} from './ReactFiberFlags';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
-import {isRootDehydrated} from './ReactFiberShellHydration';
import {
resetCurrentFiber as resetCurrentDebugFiberInDEV,
setCurrentFiber as setCurrentDebugFiberInDEV,
@@ -1878,11 +1878,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- const root: FiberRoot = finishedWork.stateNode;
- if (isRootDehydrated(root)) {
- // We've just hydrated. No need to hydrate again.
- root.isDehydrated = false;
- commitHydratedContainer(root.containerInfo);
+ if (current !== null) {
+ const prevRootState: RootState = current.memoizedState;
+ if (prevRootState.isDehydrated) {
+ const root: FiberRoot = finishedWork.stateNode;
+ commitHydratedContainer(root.containerInfo);
+ }
}
}
break;
@@ -1986,11 +1987,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- const root: FiberRoot = finishedWork.stateNode;
- if (isRootDehydrated(root)) {
- // We've just hydrated. No need to hydrate again.
- root.isDehydrated = false;
- commitHydratedContainer(root.containerInfo);
+ if (current !== null) {
+ const prevRootState: RootState = current.memoizedState;
+ if (prevRootState.isDehydrated) {
+ const root: FiberRoot = finishedWork.stateNode;
+ commitHydratedContainer(root.containerInfo);
+ }
}
}
return;
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
index af044c79ca..a53d4a2a87 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
@@ -25,6 +25,7 @@ import type {Wakeable} from 'shared/ReactTypes';
import type {OffscreenState} from './ReactFiberOffscreenComponent';
import type {HookFlags} from './ReactHookEffectTags';
import type {Cache} from './ReactFiberCacheComponent.old';
+import type {RootState} from './ReactFiberRoot.old';
import {
enableCreateEventHandleAPI,
@@ -82,7 +83,6 @@ import {
Visibility,
} from './ReactFiberFlags';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
-import {isRootDehydrated} from './ReactFiberShellHydration';
import {
resetCurrentFiber as resetCurrentDebugFiberInDEV,
setCurrentFiber as setCurrentDebugFiberInDEV,
@@ -1878,11 +1878,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- const root: FiberRoot = finishedWork.stateNode;
- if (isRootDehydrated(root)) {
- // We've just hydrated. No need to hydrate again.
- root.isDehydrated = false;
- commitHydratedContainer(root.containerInfo);
+ if (current !== null) {
+ const prevRootState: RootState = current.memoizedState;
+ if (prevRootState.isDehydrated) {
+ const root: FiberRoot = finishedWork.stateNode;
+ commitHydratedContainer(root.containerInfo);
+ }
}
}
break;
@@ -1986,11 +1987,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- const root: FiberRoot = finishedWork.stateNode;
- if (isRootDehydrated(root)) {
- // We've just hydrated. No need to hydrate again.
- root.isDehydrated = false;
- commitHydratedContainer(root.containerInfo);
+ if (current !== null) {
+ const prevRootState: RootState = current.memoizedState;
+ if (prevRootState.isDehydrated) {
+ const root: FiberRoot = finishedWork.stateNode;
+ commitHydratedContainer(root.containerInfo);
+ }
}
}
return;
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
index e00fdf5850..bea984c19f 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
@@ -8,6 +8,7 @@
*/
import type {Fiber} from './ReactInternalTypes';
+import type {RootState} from './ReactFiberRoot.new';
import type {Lanes, Lane} from './ReactFiberLane.new';
import type {
ReactScopeInstance,
@@ -160,7 +161,6 @@ import {
includesSomeLane,
mergeLanes,
} from './ReactFiberLane.new';
-import {isRootDehydrated} from './ReactFiberShellHydration';
import {resetChildFibers} from './ReactChildFiber.new';
import {createScopeInstance} from './ReactFiberScope.new';
import {transferActualDuration} from './ReactProfilerTimer.new';
@@ -891,12 +891,29 @@ function completeWork(
// If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.
markUpdate(workInProgress);
- } else if (!isRootDehydrated(fiberRoot)) {
- // Schedule an effect to clear this container at the start of the next commit.
- // This handles the case of React rendering into a container with previous children.
- // It's also safe to do for updates too, because current.child would only be null
- // if the previous render was null (so the container would already be empty).
- workInProgress.flags |= Snapshot;
+ } else {
+ if (current !== null) {
+ const prevState: RootState = current.memoizedState;
+ if (
+ // Check if this is a client root
+ !prevState.isDehydrated ||
+ // Check if we reverted to client rendering (e.g. due to an error)
+ (workInProgress.flags & ForceClientRender) !== NoFlags
+ ) {
+ // Schedule an effect to clear this container at the start of the
+ // next commit. This handles the case of React rendering into a
+ // container with previous children. It's also safe to do for
+ // updates too, because current.child would only be null if the
+ // previous render was null (so the container would already
+ // be empty).
+ workInProgress.flags |= Snapshot;
+
+ // If this was a forced client render, there may have been
+ // recoverable errors during first hydration attempt. If so, add
+ // them to a queue so we can log them in the commit phase.
+ upgradeHydrationErrorsToRecoverable();
+ }
+ }
}
}
updateHostContainer(current, workInProgress);
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
index 1c47a291ca..ef3d4f7979 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
@@ -8,6 +8,7 @@
*/
import type {Fiber} from './ReactInternalTypes';
+import type {RootState} from './ReactFiberRoot.old';
import type {Lanes, Lane} from './ReactFiberLane.old';
import type {
ReactScopeInstance,
@@ -160,7 +161,6 @@ import {
includesSomeLane,
mergeLanes,
} from './ReactFiberLane.old';
-import {isRootDehydrated} from './ReactFiberShellHydration';
import {resetChildFibers} from './ReactChildFiber.old';
import {createScopeInstance} from './ReactFiberScope.old';
import {transferActualDuration} from './ReactProfilerTimer.old';
@@ -891,12 +891,29 @@ function completeWork(
// If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.
markUpdate(workInProgress);
- } else if (!isRootDehydrated(fiberRoot)) {
- // Schedule an effect to clear this container at the start of the next commit.
- // This handles the case of React rendering into a container with previous children.
- // It's also safe to do for updates too, because current.child would only be null
- // if the previous render was null (so the container would already be empty).
- workInProgress.flags |= Snapshot;
+ } else {
+ if (current !== null) {
+ const prevState: RootState = current.memoizedState;
+ if (
+ // Check if this is a client root
+ !prevState.isDehydrated ||
+ // Check if we reverted to client rendering (e.g. due to an error)
+ (workInProgress.flags & ForceClientRender) !== NoFlags
+ ) {
+ // Schedule an effect to clear this container at the start of the
+ // next commit. This handles the case of React rendering into a
+ // container with previous children. It's also safe to do for
+ // updates too, because current.child would only be null if the
+ // previous render was null (so the container would already
+ // be empty).
+ workInProgress.flags |= Snapshot;
+
+ // If this was a forced client render, there may have been
+ // recoverable errors during first hydration attempt. If so, add
+ // them to a queue so we can log them in the commit phase.
+ upgradeHydrationErrorsToRecoverable();
+ }
+ }
}
}
updateHostContainer(current, workInProgress);
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js
index b814c35cf8..849470551a 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.new.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js
@@ -254,10 +254,12 @@ export function createContainer(
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
const hydrate = false;
+ const initialChildren = null;
return createFiberRoot(
containerInfo,
tag,
hydrate,
+ initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -285,6 +287,7 @@ export function createHydrationContainer(
containerInfo,
tag,
hydrate,
+ initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -303,9 +306,7 @@ export function createHydrationContainer(
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);
const update = createUpdate(eventTime, lane);
- // Caution: React DevTools currently depends on this property
- // being called "element".
- update.payload = {element: initialChildren};
+ update.payload = {isDehydrated: false};
update.callback =
callback !== undefined && callback !== null ? callback : null;
enqueueUpdate(current, update, lane);
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js
index 9665ca44bc..f7166095ba 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.old.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js
@@ -254,10 +254,12 @@ export function createContainer(
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
const hydrate = false;
+ const initialChildren = null;
return createFiberRoot(
containerInfo,
tag,
hydrate,
+ initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -285,6 +287,7 @@ export function createHydrationContainer(
containerInfo,
tag,
hydrate,
+ initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -303,9 +306,7 @@ export function createHydrationContainer(
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);
const update = createUpdate(eventTime, lane);
- // Caution: React DevTools currently depends on this property
- // being called "element".
- update.payload = {element: initialChildren};
+ update.payload = {isDehydrated: false};
update.callback =
callback !== undefined && callback !== null ? callback : null;
enqueueUpdate(current, update, lane);
diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js
index 00dd694be4..7ff03ceead 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.new.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.new.js
@@ -7,6 +7,7 @@
* @flow
*/
+import type {ReactNodeList} from 'shared/ReactTypes';
import type {
FiberRoot,
SuspenseHydrationCallbacks,
@@ -39,7 +40,8 @@ import {createCache, retainCache} from './ReactFiberCacheComponent.new';
export type RootState = {
element: any,
- cache: Cache | null,
+ isDehydrated: boolean,
+ cache: Cache,
transitions: Transitions | null,
};
@@ -59,7 +61,6 @@ function FiberRootNode(
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
- this.isDehydrated = hydrate;
this.callbackNode = null;
this.callbackPriority = NoLane;
this.eventTimes = createLaneMap(NoLanes);
@@ -128,6 +129,7 @@ export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
+ initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
@@ -178,15 +180,17 @@ export function createFiberRoot(
root.pooledCache = initialCache;
retainCache(initialCache);
const initialState: RootState = {
- element: null,
+ element: initialChildren,
+ isDehydrated: hydrate,
cache: initialCache,
transitions: null,
};
uninitializedFiber.memoizedState = initialState;
} else {
const initialState: RootState = {
- element: null,
- cache: null,
+ element: initialChildren,
+ isDehydrated: hydrate,
+ cache: (null: any), // not enabled yet
transitions: null,
};
uninitializedFiber.memoizedState = initialState;
diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js
index 1e561e49fa..179b9c17ae 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.old.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.old.js
@@ -7,6 +7,7 @@
* @flow
*/
+import type {ReactNodeList} from 'shared/ReactTypes';
import type {
FiberRoot,
SuspenseHydrationCallbacks,
@@ -39,7 +40,8 @@ import {createCache, retainCache} from './ReactFiberCacheComponent.old';
export type RootState = {
element: any,
- cache: Cache | null,
+ isDehydrated: boolean,
+ cache: Cache,
transitions: Transitions | null,
};
@@ -59,7 +61,6 @@ function FiberRootNode(
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
- this.isDehydrated = hydrate;
this.callbackNode = null;
this.callbackPriority = NoLane;
this.eventTimes = createLaneMap(NoLanes);
@@ -128,6 +129,7 @@ export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
+ initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
@@ -178,15 +180,17 @@ export function createFiberRoot(
root.pooledCache = initialCache;
retainCache(initialCache);
const initialState: RootState = {
- element: null,
+ element: initialChildren,
+ isDehydrated: hydrate,
cache: initialCache,
transitions: null,
};
uninitializedFiber.memoizedState = initialState;
} else {
const initialState: RootState = {
- element: null,
- cache: null,
+ element: initialChildren,
+ isDehydrated: hydrate,
+ cache: (null: any), // not enabled yet
transitions: null,
};
uninitializedFiber.memoizedState = initialState;
diff --git a/packages/react-reconciler/src/ReactFiberShellHydration.js b/packages/react-reconciler/src/ReactFiberShellHydration.js
index 2f1afde6de..caadb978f6 100644
--- a/packages/react-reconciler/src/ReactFiberShellHydration.js
+++ b/packages/react-reconciler/src/ReactFiberShellHydration.js
@@ -8,10 +8,12 @@
*/
import type {FiberRoot} from './ReactInternalTypes';
+import type {RootState} from './ReactFiberRoot.new';
// This is imported by the event replaying implementation in React DOM. It's
// in a separate file to break a circular dependency between the renderer and
// the reconciler.
export function isRootDehydrated(root: FiberRoot) {
- return root.isDehydrated;
+ const currentState: RootState = root.current.memoizedState;
+ return currentState.isDehydrated;
}
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index caab7a666f..558440effa 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -110,6 +110,7 @@ import {
StoreConsistency,
HostEffectMask,
Hydrating,
+ ForceClientRender,
BeforeMutationMask,
MutationMask,
LayoutMask,
@@ -582,34 +583,7 @@ export function scheduleUpdateOnFiber(
}
}
- if (isRootDehydrated(root) && root.tag !== LegacyRoot) {
- // This root's shell hasn't hydrated yet. Revert to client rendering.
- if (workInProgressRoot === root) {
- // If this happened during an interleaved event, interrupt the
- // in-progress hydration. Theoretically, we could attempt to force a
- // synchronous hydration before switching to client rendering, but the
- // most common reason the shell hasn't hydrated yet is because it
- // suspended. So it's very likely to suspend again anyway. For
- // simplicity, we'll skip that atttempt and go straight to
- // client rendering.
- //
- // Another way to model this would be to give the initial hydration its
- // own special lane. However, it may not be worth adding a lane solely
- // for this purpose, so we'll wait until we find another use case before
- // adding it.
- //
- // TODO: Consider only interrupting hydration if the priority of the
- // update is higher than default.
- prepareFreshStack(root, NoLanes);
- }
- root.isDehydrated = false;
- const error = new Error(
- 'This root received an early update, before anything was able ' +
- 'hydrate. Switched the entire root to client rendering.',
- );
- const onRecoverableError = root.onRecoverableError;
- onRecoverableError(error);
- } else if (root === workInProgressRoot) {
+ if (root === workInProgressRoot) {
// TODO: Consolidate with `isInterleavedUpdate` check
// Received an update to a tree that's in the middle of rendering. Mark
@@ -1017,28 +991,42 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
function recoverFromConcurrentError(root, errorRetryLanes) {
// If an error occurred during hydration, discard server response and fall
// back to client side render.
+
+ // Before rendering again, save the errors from the previous attempt.
+ const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
+
if (isRootDehydrated(root)) {
- root.isDehydrated = false;
+ // The shell failed to hydrate. Set a flag to force a client rendering
+ // during the next attempt. To do this, we call prepareFreshStack now
+ // to create the root work-in-progress fiber. This is a bit weird in terms
+ // of factoring, because it relies on renderRootSync not calling
+ // prepareFreshStack again in the call below, which happens because the
+ // root and lanes haven't changed.
+ //
+ // TODO: I think what we should do is set ForceClientRender inside
+ // throwException, like we do for nested Suspense boundaries. The reason
+ // it's here instead is so we can switch to the synchronous work loop, too.
+ // Something to consider for a future refactor.
+ const rootWorkInProgress = prepareFreshStack(root, errorRetryLanes);
+ rootWorkInProgress.flags |= ForceClientRender;
if (__DEV__) {
errorHydratingContainer(root.containerInfo);
}
- const error = new Error(
- 'There was an error while hydrating. Because the error happened outside ' +
- 'of a Suspense boundary, the entire root will switch to ' +
- 'client rendering.',
- );
- renderDidError(error);
}
- const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
const exitStatus = renderRootSync(root, errorRetryLanes);
if (exitStatus !== RootErrored) {
// Successfully finished rendering on retry
- if (errorsFromFirstAttempt !== null) {
- // The errors from the failed first attempt have been recovered. Add
- // them to the collection of recoverable errors. We'll log them in the
- // commit phase.
- queueRecoverableErrors(errorsFromFirstAttempt);
+
+ // The errors from the failed first attempt have been recovered. Add
+ // them to the collection of recoverable errors. We'll log them in the
+ // commit phase.
+ const errorsFromSecondAttempt = workInProgressRootRecoverableErrors;
+ workInProgressRootRecoverableErrors = errorsFromFirstAttempt;
+ // The errors from the second attempt should be queued after the errors
+ // from the first attempt, to preserve the causal sequence.
+ if (errorsFromSecondAttempt !== null) {
+ queueRecoverableErrors(errorsFromSecondAttempt);
}
} else {
// The UI failed to recover.
@@ -1454,7 +1442,7 @@ export function popRenderLanes(fiber: Fiber) {
popFromStack(subtreeRenderLanesCursor, fiber);
}
-function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
+function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
@@ -1480,7 +1468,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
}
}
workInProgressRoot = root;
- workInProgress = createWorkInProgress(root.current, null);
+ const rootWorkInProgress = createWorkInProgress(root.current, null);
+ workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
@@ -1496,6 +1485,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
if (__DEV__) {
ReactStrictModeWarnings.discardPendingWarnings();
}
+
+ return rootWorkInProgress;
}
function handleError(root, thrownValue): void {
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index 04a324cdc8..c1c090d82b 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -110,6 +110,7 @@ import {
StoreConsistency,
HostEffectMask,
Hydrating,
+ ForceClientRender,
BeforeMutationMask,
MutationMask,
LayoutMask,
@@ -582,34 +583,7 @@ export function scheduleUpdateOnFiber(
}
}
- if (isRootDehydrated(root) && root.tag !== LegacyRoot) {
- // This root's shell hasn't hydrated yet. Revert to client rendering.
- if (workInProgressRoot === root) {
- // If this happened during an interleaved event, interrupt the
- // in-progress hydration. Theoretically, we could attempt to force a
- // synchronous hydration before switching to client rendering, but the
- // most common reason the shell hasn't hydrated yet is because it
- // suspended. So it's very likely to suspend again anyway. For
- // simplicity, we'll skip that atttempt and go straight to
- // client rendering.
- //
- // Another way to model this would be to give the initial hydration its
- // own special lane. However, it may not be worth adding a lane solely
- // for this purpose, so we'll wait until we find another use case before
- // adding it.
- //
- // TODO: Consider only interrupting hydration if the priority of the
- // update is higher than default.
- prepareFreshStack(root, NoLanes);
- }
- root.isDehydrated = false;
- const error = new Error(
- 'This root received an early update, before anything was able ' +
- 'hydrate. Switched the entire root to client rendering.',
- );
- const onRecoverableError = root.onRecoverableError;
- onRecoverableError(error);
- } else if (root === workInProgressRoot) {
+ if (root === workInProgressRoot) {
// TODO: Consolidate with `isInterleavedUpdate` check
// Received an update to a tree that's in the middle of rendering. Mark
@@ -1017,28 +991,42 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
function recoverFromConcurrentError(root, errorRetryLanes) {
// If an error occurred during hydration, discard server response and fall
// back to client side render.
+
+ // Before rendering again, save the errors from the previous attempt.
+ const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
+
if (isRootDehydrated(root)) {
- root.isDehydrated = false;
+ // The shell failed to hydrate. Set a flag to force a client rendering
+ // during the next attempt. To do this, we call prepareFreshStack now
+ // to create the root work-in-progress fiber. This is a bit weird in terms
+ // of factoring, because it relies on renderRootSync not calling
+ // prepareFreshStack again in the call below, which happens because the
+ // root and lanes haven't changed.
+ //
+ // TODO: I think what we should do is set ForceClientRender inside
+ // throwException, like we do for nested Suspense boundaries. The reason
+ // it's here instead is so we can switch to the synchronous work loop, too.
+ // Something to consider for a future refactor.
+ const rootWorkInProgress = prepareFreshStack(root, errorRetryLanes);
+ rootWorkInProgress.flags |= ForceClientRender;
if (__DEV__) {
errorHydratingContainer(root.containerInfo);
}
- const error = new Error(
- 'There was an error while hydrating. Because the error happened outside ' +
- 'of a Suspense boundary, the entire root will switch to ' +
- 'client rendering.',
- );
- renderDidError(error);
}
- const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
const exitStatus = renderRootSync(root, errorRetryLanes);
if (exitStatus !== RootErrored) {
// Successfully finished rendering on retry
- if (errorsFromFirstAttempt !== null) {
- // The errors from the failed first attempt have been recovered. Add
- // them to the collection of recoverable errors. We'll log them in the
- // commit phase.
- queueRecoverableErrors(errorsFromFirstAttempt);
+
+ // The errors from the failed first attempt have been recovered. Add
+ // them to the collection of recoverable errors. We'll log them in the
+ // commit phase.
+ const errorsFromSecondAttempt = workInProgressRootRecoverableErrors;
+ workInProgressRootRecoverableErrors = errorsFromFirstAttempt;
+ // The errors from the second attempt should be queued after the errors
+ // from the first attempt, to preserve the causal sequence.
+ if (errorsFromSecondAttempt !== null) {
+ queueRecoverableErrors(errorsFromSecondAttempt);
}
} else {
// The UI failed to recover.
@@ -1454,7 +1442,7 @@ export function popRenderLanes(fiber: Fiber) {
popFromStack(subtreeRenderLanesCursor, fiber);
}
-function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
+function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
@@ -1480,7 +1468,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
}
}
workInProgressRoot = root;
- workInProgress = createWorkInProgress(root.current, null);
+ const rootWorkInProgress = createWorkInProgress(root.current, null);
+ workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
@@ -1496,6 +1485,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
if (__DEV__) {
ReactStrictModeWarnings.discardPendingWarnings();
}
+
+ return rootWorkInProgress;
}
function handleError(root, thrownValue): void {
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index dd2e09c03b..1fa3d4b668 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -213,8 +213,6 @@ type BaseFiberRootProperties = {|
// Top context object, used by renderSubtreeIntoContainer
context: Object | null,
pendingContext: Object | null,
- // Determines if we should attempt to hydrate on the initial mount
- +isDehydrated: boolean,
// Used by useMutableSource hook to avoid tearing during hydration.
mutableSourceEagerHydrationData?: Array<