diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js
index 44cdf4f240a21..9d1b6a16c2038 100644
--- a/packages/react-art/src/ReactART.js
+++ b/packages/react-art/src/ReactART.js
@@ -69,6 +69,7 @@ class Surface extends React.Component {
this._mountNode = createContainer(
this._surface,
LegacyRoot,
+ false,
null,
false,
false,
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
index 3584715f91e2f..f3ff64daeec43 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
@@ -9,7 +9,6 @@
let JSDOM;
let React;
-let startTransition;
let ReactDOMClient;
let Scheduler;
let clientAct;
@@ -34,8 +33,6 @@ describe('ReactDOMFizzShellHydration', () => {
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
- startTransition = React.startTransition;
-
textCache = new Map();
// Test Environment
@@ -217,36 +214,7 @@ describe('ReactDOMFizzShellHydration', () => {
expect(container.textContent).toBe('Shell');
});
- 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 () => {
+ test('updating the root before the shell hydrates forces a client render', async () => {
function App() {
return ;
}
@@ -277,9 +245,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-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
index 9d6a38188376d..df693b8784992 100644
--- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
@@ -253,15 +253,6 @@ describe('ReactDOMRoot', () => {
);
});
- it('callback passed to legacy hydrate() API', () => {
- container.innerHTML = '
Hi
';
- ReactDOM.hydrate(Hi
, container, () => {
- Scheduler.unstable_yieldValue('callback');
- });
- expect(container.textContent).toEqual('Hi');
- expect(Scheduler).toHaveYielded(['callback']);
- });
-
it('warns when unmounting with legacy API (no previous content)', () => {
const root = ReactDOMClient.createRoot(container);
root.render(Hi
);
diff --git a/packages/react-dom/src/client/ReactDOMLegacy.js b/packages/react-dom/src/client/ReactDOMLegacy.js
index af0e35e128bd4..3b751405a3034 100644
--- a/packages/react-dom/src/client/ReactDOMLegacy.js
+++ b/packages/react-dom/src/client/ReactDOMLegacy.js
@@ -27,7 +27,6 @@ import {
import {
createContainer,
- createHydrationContainer,
findHostInstanceWithNoPortals,
updateContainer,
flushSync,
@@ -110,81 +109,34 @@ function noopOnRecoverableError() {
function legacyCreateRootFromDOMContainer(
container: Container,
- initialChildren: ReactNodeList,
- parentComponent: ?React$Component,
- callback: ?Function,
- isHydrationContainer: boolean,
+ forceHydrate: boolean,
): FiberRoot {
- if (isHydrationContainer) {
- if (typeof callback === 'function') {
- const originalCallback = callback;
- callback = function() {
- const instance = getPublicRootInstance(root);
- originalCallback.call(instance);
- };
- }
-
- const root = createHydrationContainer(
- initialChildren,
- callback,
- container,
- LegacyRoot,
- null, // hydrationCallbacks
- false, // isStrictMode
- false, // concurrentUpdatesByDefaultOverride,
- '', // identifierPrefix
- noopOnRecoverableError,
- // TODO(luna) Support hydration later
- null,
- );
- container._reactRootContainer = root;
- markContainerAsRoot(root.current, container);
-
- const rootContainerElement =
- container.nodeType === COMMENT_NODE ? container.parentNode : container;
- listenToAllSupportedEvents(rootContainerElement);
-
- flushSync();
- return root;
- } else {
- // First clear any existing content.
+ // First clear any existing content.
+ if (!forceHydrate) {
let rootSibling;
while ((rootSibling = container.lastChild)) {
container.removeChild(rootSibling);
}
+ }
- if (typeof callback === 'function') {
- const originalCallback = callback;
- callback = function() {
- const instance = getPublicRootInstance(root);
- originalCallback.call(instance);
- };
- }
-
- const root = createContainer(
- container,
- LegacyRoot,
- null, // hydrationCallbacks
- false, // isStrictMode
- false, // concurrentUpdatesByDefaultOverride,
- '', // identifierPrefix
- noopOnRecoverableError, // onRecoverableError
- null, // transitionCallbacks
- );
- container._reactRootContainer = root;
- markContainerAsRoot(root.current, container);
-
- const rootContainerElement =
- container.nodeType === COMMENT_NODE ? container.parentNode : container;
- listenToAllSupportedEvents(rootContainerElement);
+ const root = createContainer(
+ container,
+ LegacyRoot,
+ forceHydrate,
+ null, // hydrationCallbacks
+ false, // isStrictMode
+ false, // concurrentUpdatesByDefaultOverride,
+ '', // identifierPrefix
+ noopOnRecoverableError, // onRecoverableError
+ null, // transitionCallbacks
+ );
+ markContainerAsRoot(root.current, container);
- // Initial mount should not be batched.
- flushSync(() => {
- updateContainer(initialChildren, root, parentComponent, callback);
- });
+ const rootContainerElement =
+ container.nodeType === COMMENT_NODE ? container.parentNode : container;
+ listenToAllSupportedEvents(rootContainerElement);
- return root;
- }
+ return root;
}
function warnOnInvalidCallback(callback: mixed, callerName: string): void {
@@ -212,30 +164,39 @@ function legacyRenderSubtreeIntoContainer(
warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
}
- const maybeRoot = container._reactRootContainer;
- let root: FiberRoot;
- if (!maybeRoot) {
+ let root = container._reactRootContainer;
+ let fiberRoot: FiberRoot;
+ if (!root) {
// Initial mount
- root = legacyCreateRootFromDOMContainer(
+ root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
- children,
- parentComponent,
- callback,
forceHydrate,
);
+ fiberRoot = root;
+ if (typeof callback === 'function') {
+ const originalCallback = callback;
+ callback = function() {
+ const instance = getPublicRootInstance(fiberRoot);
+ originalCallback.call(instance);
+ };
+ }
+ // Initial mount should not be batched.
+ flushSync(() => {
+ updateContainer(children, fiberRoot, parentComponent, callback);
+ });
} else {
- root = maybeRoot;
+ fiberRoot = root;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
- const instance = getPublicRootInstance(root);
+ const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Update
- updateContainer(children, root, parentComponent, callback);
+ updateContainer(children, fiberRoot, parentComponent, callback);
}
- return getPublicRootInstance(root);
+ return getPublicRootInstance(fiberRoot);
}
export function findDOMNode(
diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js
index 6e7192ae10453..d71de3bb0c26d 100644
--- a/packages/react-dom/src/client/ReactDOMRoot.js
+++ b/packages/react-dom/src/client/ReactDOMRoot.js
@@ -225,6 +225,7 @@ export function createRoot(
const root = createContainer(
container,
ConcurrentRoot,
+ false,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -301,7 +302,6 @@ export function hydrateRoot(
const root = createHydrationContainer(
initialChildren,
- null,
container,
ConcurrentRoot,
hydrationCallbacks,
diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js
index e2974586ec6ac..f8b8231f8f40e 100644
--- a/packages/react-dom/src/events/ReactDOMEventListener.js
+++ b/packages/react-dom/src/events/ReactDOMEventListener.js
@@ -53,7 +53,6 @@ import {
setCurrentUpdatePriority,
} from 'react-reconciler/src/ReactEventPriorities';
import ReactSharedInternals from 'shared/ReactSharedInternals';
-import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration';
const {ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -387,7 +386,7 @@ export function findInstanceBlockingEvent(
targetInst = null;
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
- if (isRootDehydrated(root)) {
+ if (root.isDehydrated) {
// If this happens during a replay something went wrong and it might block
// the whole system.
return getContainerFromFiber(nearestMounted);
diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js
index 9de82a99a7be3..744f5dfda9d9b 100644
--- a/packages/react-dom/src/events/ReactDOMEventReplaying.js
+++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js
@@ -39,7 +39,6 @@ import {
} from '../client/ReactDOMComponentTree';
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
import {isHigherEventPriority} from 'react-reconciler/src/ReactEventPriorities';
-import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration';
let _attemptSynchronousHydration: (fiber: Object) => void;
@@ -415,7 +414,7 @@ function attemptExplicitHydrationTarget(
}
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
- if (isRootDehydrated(root)) {
+ if (root.isDehydrated) {
queuedTarget.blockedOn = getContainerFromFiber(nearestMounted);
// We don't currently have a way to increase the priority of
// a root other than sync.
diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js
index e08a98653fb6c..127b20fd5dacf 100644
--- a/packages/react-native-renderer/src/ReactFabric.js
+++ b/packages/react-native-renderer/src/ReactFabric.js
@@ -215,6 +215,7 @@ function render(
root = createContainer(
containerTag,
concurrentRoot ? ConcurrentRoot : LegacyRoot,
+ false,
null,
false,
null,
diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js
index e751195dda00a..1dca2cd7a1d93 100644
--- a/packages/react-native-renderer/src/ReactNativeRenderer.js
+++ b/packages/react-native-renderer/src/ReactNativeRenderer.js
@@ -211,6 +211,7 @@ function render(
root = createContainer(
containerTag,
LegacyRoot,
+ false,
null,
false,
null,
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index 8e4050dcfa336..e0ba72076a1af 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -974,6 +974,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
root = NoopRenderer.createContainer(
container,
tag,
+ false,
null,
null,
false,
@@ -995,6 +996,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
const fiberRoot = NoopRenderer.createContainer(
container,
ConcurrentRoot,
+ false,
null,
null,
false,
@@ -1027,6 +1029,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
const fiberRoot = NoopRenderer.createContainer(
container,
LegacyRoot,
+ false,
null,
null,
false,
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 7dc29bd2b0f08..4bae5d0b7b982 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -7,11 +7,7 @@
* @flow
*/
-import type {
- ReactProviderType,
- ReactContext,
- ReactNodeList,
-} from 'shared/ReactTypes';
+import type {ReactProviderType, ReactContext} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {TypeOfMode} from './ReactTypeOfMode';
@@ -33,7 +29,6 @@ import type {
SpawnedCachePool,
} from './ReactFiberCacheComponent.new';
import type {UpdateQueue} from './ReactUpdateQueue.new';
-import type {RootState} from './ReactFiberRoot.new';
import {
enableSuspenseAvoidThisFallback,
enableCPUSuspense,
@@ -1316,7 +1311,7 @@ function pushHostRootContext(workInProgress) {
function updateHostRoot(current, workInProgress, renderLanes) {
pushHostRootContext(workInProgress);
- const updateQueue: UpdateQueue = (workInProgress.updateQueue: any);
+ const updateQueue = workInProgress.updateQueue;
if (current === null || updateQueue === null) {
throw new Error(
@@ -1331,7 +1326,7 @@ function updateHostRoot(current, workInProgress, renderLanes) {
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
- const nextState: RootState = workInProgress.memoizedState;
+ const nextState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
@@ -1346,130 +1341,64 @@ 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 (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);
- }
+ if (nextChildren === prevChildren) {
+ resetHydrationState();
+ return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
+ }
+ if (root.isDehydrated && 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);
}
}
+ }
- 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 {
- // Root is not dehydrated. Either this is a client-only root, or it
- // already hydrated.
- resetHydrationState();
- if (nextChildren === prevChildren) {
- return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
- }
+ // Otherwise reset hydration state in case we aborted and resumed another
+ // root.
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
+ resetHydrationState();
}
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 206773b03b334..583539dd08b52 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -7,11 +7,7 @@
* @flow
*/
-import type {
- ReactProviderType,
- ReactContext,
- ReactNodeList,
-} from 'shared/ReactTypes';
+import type {ReactProviderType, ReactContext} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {TypeOfMode} from './ReactTypeOfMode';
@@ -33,7 +29,6 @@ import type {
SpawnedCachePool,
} from './ReactFiberCacheComponent.old';
import type {UpdateQueue} from './ReactUpdateQueue.old';
-import type {RootState} from './ReactFiberRoot.old';
import {
enableSuspenseAvoidThisFallback,
enableCPUSuspense,
@@ -1316,7 +1311,7 @@ function pushHostRootContext(workInProgress) {
function updateHostRoot(current, workInProgress, renderLanes) {
pushHostRootContext(workInProgress);
- const updateQueue: UpdateQueue = (workInProgress.updateQueue: any);
+ const updateQueue = workInProgress.updateQueue;
if (current === null || updateQueue === null) {
throw new Error(
@@ -1331,7 +1326,7 @@ function updateHostRoot(current, workInProgress, renderLanes) {
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
- const nextState: RootState = workInProgress.memoizedState;
+ const nextState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
@@ -1346,130 +1341,64 @@ 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 (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);
- }
+ if (nextChildren === prevChildren) {
+ resetHydrationState();
+ return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
+ }
+ if (root.isDehydrated && 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);
}
}
+ }
- 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 {
- // Root is not dehydrated. Either this is a client-only root, or it
- // already hydrated.
- resetHydrationState();
- if (nextChildren === prevChildren) {
- return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
- }
+ // Otherwise reset hydration state in case we aborted and resumed another
+ // root.
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
+ resetHydrationState();
}
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 776c402a00eb9..75553555b928d 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
@@ -25,7 +25,6 @@ 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,
@@ -1878,12 +1877,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- if (current !== null) {
- const prevRootState: RootState = current.memoizedState;
- if (prevRootState.isDehydrated) {
- const root: FiberRoot = finishedWork.stateNode;
- commitHydratedContainer(root.containerInfo);
- }
+ const root: FiberRoot = finishedWork.stateNode;
+ if (root.isDehydrated) {
+ // We've just hydrated. No need to hydrate again.
+ root.isDehydrated = false;
+ commitHydratedContainer(root.containerInfo);
}
}
break;
@@ -1987,12 +1985,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- if (current !== null) {
- const prevRootState: RootState = current.memoizedState;
- if (prevRootState.isDehydrated) {
- const root: FiberRoot = finishedWork.stateNode;
- commitHydratedContainer(root.containerInfo);
- }
+ const root: FiberRoot = finishedWork.stateNode;
+ if (root.isDehydrated) {
+ // We've just hydrated. No need to hydrate again.
+ root.isDehydrated = false;
+ commitHydratedContainer(root.containerInfo);
}
}
return;
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
index a53d4a2a87525..23e9d6070c9a2 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
@@ -25,7 +25,6 @@ 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,
@@ -1878,12 +1877,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- if (current !== null) {
- const prevRootState: RootState = current.memoizedState;
- if (prevRootState.isDehydrated) {
- const root: FiberRoot = finishedWork.stateNode;
- commitHydratedContainer(root.containerInfo);
- }
+ const root: FiberRoot = finishedWork.stateNode;
+ if (root.isDehydrated) {
+ // We've just hydrated. No need to hydrate again.
+ root.isDehydrated = false;
+ commitHydratedContainer(root.containerInfo);
}
}
break;
@@ -1987,12 +1985,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- if (current !== null) {
- const prevRootState: RootState = current.memoizedState;
- if (prevRootState.isDehydrated) {
- const root: FiberRoot = finishedWork.stateNode;
- commitHydratedContainer(root.containerInfo);
- }
+ const root: FiberRoot = finishedWork.stateNode;
+ if (root.isDehydrated) {
+ // We've just hydrated. No need to hydrate again.
+ root.isDehydrated = false;
+ commitHydratedContainer(root.containerInfo);
}
}
return;
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
index bea984c19f1ce..2a44cf94a14aa 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
@@ -8,7 +8,6 @@
*/
import type {Fiber} from './ReactInternalTypes';
-import type {RootState} from './ReactFiberRoot.new';
import type {Lanes, Lane} from './ReactFiberLane.new';
import type {
ReactScopeInstance,
@@ -891,29 +890,12 @@ 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 (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();
- }
- }
+ } else if (!fiberRoot.isDehydrated) {
+ // 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;
}
}
updateHostContainer(current, workInProgress);
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
index ef3d4f7979f29..f02a20222d0fe 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
@@ -8,7 +8,6 @@
*/
import type {Fiber} from './ReactInternalTypes';
-import type {RootState} from './ReactFiberRoot.old';
import type {Lanes, Lane} from './ReactFiberLane.old';
import type {
ReactScopeInstance,
@@ -891,29 +890,12 @@ 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 (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();
- }
- }
+ } else if (!fiberRoot.isDehydrated) {
+ // 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;
}
}
updateHostContainer(current, workInProgress);
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js
index 849470551a2bc..8607b227e9b40 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.new.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js
@@ -48,7 +48,6 @@ import {
isContextProvider as isLegacyContextProvider,
} from './ReactFiberContext.new';
import {createFiberRoot} from './ReactFiberRoot.new';
-import {isRootDehydrated} from './ReactFiberShellHydration';
import {
injectInternals,
markRenderScheduled,
@@ -246,6 +245,9 @@ function findHostInstanceWithWarning(
export function createContainer(
containerInfo: Container,
tag: RootTag,
+ // TODO: We can remove hydration-specific stuff from createContainer once
+ // we delete legacy mode. The new root API uses createHydrationContainer.
+ hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
@@ -253,13 +255,10 @@ export function createContainer(
onRecoverableError: (error: mixed) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
- const hydrate = false;
- const initialChildren = null;
return createFiberRoot(
containerInfo,
tag,
hydrate,
- initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -271,8 +270,6 @@ export function createContainer(
export function createHydrationContainer(
initialChildren: ReactNodeList,
- // TODO: Remove `callback` when we delete legacy mode.
- callback: ?Function,
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
@@ -287,7 +284,6 @@ export function createHydrationContainer(
containerInfo,
tag,
hydrate,
- initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -306,9 +302,9 @@ export function createHydrationContainer(
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);
const update = createUpdate(eventTime, lane);
- update.payload = {isDehydrated: false};
- update.callback =
- callback !== undefined && callback !== null ? callback : null;
+ // Caution: React DevTools currently depends on this property
+ // being called "element".
+ update.payload = {element: initialChildren};
enqueueUpdate(current, update, lane);
scheduleInitialHydrationOnRoot(root, lane, eventTime);
@@ -413,7 +409,7 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
switch (fiber.tag) {
case HostRoot:
const root: FiberRoot = fiber.stateNode;
- if (isRootDehydrated(root)) {
+ if (root.isDehydrated) {
// Flush the first scheduled "update".
const lanes = getHighestPriorityPendingLanes(root);
flushRoot(root, lanes);
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js
index f7166095ba13a..4970b685b1c1a 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.old.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js
@@ -48,7 +48,6 @@ import {
isContextProvider as isLegacyContextProvider,
} from './ReactFiberContext.old';
import {createFiberRoot} from './ReactFiberRoot.old';
-import {isRootDehydrated} from './ReactFiberShellHydration';
import {
injectInternals,
markRenderScheduled,
@@ -246,6 +245,9 @@ function findHostInstanceWithWarning(
export function createContainer(
containerInfo: Container,
tag: RootTag,
+ // TODO: We can remove hydration-specific stuff from createContainer once
+ // we delete legacy mode. The new root API uses createHydrationContainer.
+ hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
@@ -253,13 +255,10 @@ export function createContainer(
onRecoverableError: (error: mixed) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
- const hydrate = false;
- const initialChildren = null;
return createFiberRoot(
containerInfo,
tag,
hydrate,
- initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -271,8 +270,6 @@ export function createContainer(
export function createHydrationContainer(
initialChildren: ReactNodeList,
- // TODO: Remove `callback` when we delete legacy mode.
- callback: ?Function,
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
@@ -287,7 +284,6 @@ export function createHydrationContainer(
containerInfo,
tag,
hydrate,
- initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -306,9 +302,9 @@ export function createHydrationContainer(
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);
const update = createUpdate(eventTime, lane);
- update.payload = {isDehydrated: false};
- update.callback =
- callback !== undefined && callback !== null ? callback : null;
+ // Caution: React DevTools currently depends on this property
+ // being called "element".
+ update.payload = {element: initialChildren};
enqueueUpdate(current, update, lane);
scheduleInitialHydrationOnRoot(root, lane, eventTime);
@@ -413,7 +409,7 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
switch (fiber.tag) {
case HostRoot:
const root: FiberRoot = fiber.stateNode;
- if (isRootDehydrated(root)) {
+ if (root.isDehydrated) {
// Flush the first scheduled "update".
const lanes = getHighestPriorityPendingLanes(root);
flushRoot(root, lanes);
diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js
index 7ff03ceead0e3..00dd694be4f5f 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.new.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.new.js
@@ -7,7 +7,6 @@
* @flow
*/
-import type {ReactNodeList} from 'shared/ReactTypes';
import type {
FiberRoot,
SuspenseHydrationCallbacks,
@@ -40,8 +39,7 @@ import {createCache, retainCache} from './ReactFiberCacheComponent.new';
export type RootState = {
element: any,
- isDehydrated: boolean,
- cache: Cache,
+ cache: Cache | null,
transitions: Transitions | null,
};
@@ -61,6 +59,7 @@ function FiberRootNode(
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
+ this.isDehydrated = hydrate;
this.callbackNode = null;
this.callbackPriority = NoLane;
this.eventTimes = createLaneMap(NoLanes);
@@ -129,7 +128,6 @@ export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
- initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
@@ -180,17 +178,15 @@ export function createFiberRoot(
root.pooledCache = initialCache;
retainCache(initialCache);
const initialState: RootState = {
- element: initialChildren,
- isDehydrated: hydrate,
+ element: null,
cache: initialCache,
transitions: null,
};
uninitializedFiber.memoizedState = initialState;
} else {
const initialState: RootState = {
- element: initialChildren,
- isDehydrated: hydrate,
- cache: (null: any), // not enabled yet
+ element: null,
+ cache: null,
transitions: null,
};
uninitializedFiber.memoizedState = initialState;
diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js
index 179b9c17ae416..1e561e49facb3 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.old.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.old.js
@@ -7,7 +7,6 @@
* @flow
*/
-import type {ReactNodeList} from 'shared/ReactTypes';
import type {
FiberRoot,
SuspenseHydrationCallbacks,
@@ -40,8 +39,7 @@ import {createCache, retainCache} from './ReactFiberCacheComponent.old';
export type RootState = {
element: any,
- isDehydrated: boolean,
- cache: Cache,
+ cache: Cache | null,
transitions: Transitions | null,
};
@@ -61,6 +59,7 @@ function FiberRootNode(
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
+ this.isDehydrated = hydrate;
this.callbackNode = null;
this.callbackPriority = NoLane;
this.eventTimes = createLaneMap(NoLanes);
@@ -129,7 +128,6 @@ export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
- initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
@@ -180,17 +178,15 @@ export function createFiberRoot(
root.pooledCache = initialCache;
retainCache(initialCache);
const initialState: RootState = {
- element: initialChildren,
- isDehydrated: hydrate,
+ element: null,
cache: initialCache,
transitions: null,
};
uninitializedFiber.memoizedState = initialState;
} else {
const initialState: RootState = {
- element: initialChildren,
- isDehydrated: hydrate,
- cache: (null: any), // not enabled yet
+ element: null,
+ cache: null,
transitions: null,
};
uninitializedFiber.memoizedState = initialState;
diff --git a/packages/react-reconciler/src/ReactFiberShellHydration.js b/packages/react-reconciler/src/ReactFiberShellHydration.js
deleted file mode 100644
index caadb978f69d0..0000000000000
--- a/packages/react-reconciler/src/ReactFiberShellHydration.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- * @flow
- */
-
-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) {
- 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 558440effa77a..7223ad7d052b0 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -88,7 +88,6 @@ import {
createWorkInProgress,
assignFiberPropertiesInDEV,
} from './ReactFiber.new';
-import {isRootDehydrated} from './ReactFiberShellHydration';
import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode';
import {
HostRoot,
@@ -110,7 +109,6 @@ import {
StoreConsistency,
HostEffectMask,
Hydrating,
- ForceClientRender,
BeforeMutationMask,
MutationMask,
LayoutMask,
@@ -583,7 +581,34 @@ export function scheduleUpdateOnFiber(
}
}
- if (root === workInProgressRoot) {
+ if (root.isDehydrated && 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) {
// TODO: Consolidate with `isInterleavedUpdate` check
// Received an update to a tree that's in the middle of rendering. Mark
@@ -991,42 +1016,28 @@ 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)) {
- // 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 (root.isDehydrated) {
+ root.isDehydrated = false;
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
-
- // 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);
+ 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);
}
} else {
// The UI failed to recover.
@@ -1442,7 +1453,7 @@ export function popRenderLanes(fiber: Fiber) {
popFromStack(subtreeRenderLanesCursor, fiber);
}
-function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
+function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
@@ -1468,8 +1479,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
}
}
workInProgressRoot = root;
- const rootWorkInProgress = createWorkInProgress(root.current, null);
- workInProgress = rootWorkInProgress;
+ workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
@@ -1485,8 +1495,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
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 c1c090d82b0b5..d8bb6b16e29fb 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -88,7 +88,6 @@ import {
createWorkInProgress,
assignFiberPropertiesInDEV,
} from './ReactFiber.old';
-import {isRootDehydrated} from './ReactFiberShellHydration';
import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode';
import {
HostRoot,
@@ -110,7 +109,6 @@ import {
StoreConsistency,
HostEffectMask,
Hydrating,
- ForceClientRender,
BeforeMutationMask,
MutationMask,
LayoutMask,
@@ -583,7 +581,34 @@ export function scheduleUpdateOnFiber(
}
}
- if (root === workInProgressRoot) {
+ if (root.isDehydrated && 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) {
// TODO: Consolidate with `isInterleavedUpdate` check
// Received an update to a tree that's in the middle of rendering. Mark
@@ -991,42 +1016,28 @@ 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)) {
- // 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 (root.isDehydrated) {
+ root.isDehydrated = false;
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
-
- // 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);
+ 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);
}
} else {
// The UI failed to recover.
@@ -1442,7 +1453,7 @@ export function popRenderLanes(fiber: Fiber) {
popFromStack(subtreeRenderLanesCursor, fiber);
}
-function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
+function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
@@ -1468,8 +1479,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
}
}
workInProgressRoot = root;
- const rootWorkInProgress = createWorkInProgress(root.current, null);
- workInProgress = rootWorkInProgress;
+ workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
@@ -1485,8 +1495,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
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 1fa3d4b6680d1..dd2e09c03b210 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -213,6 +213,8 @@ 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<
diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
index 82e23de9965da..d0c3d5b236ea4 100644
--- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
@@ -72,6 +72,7 @@ describe('ReactFiberHostContext', () => {
const container = Renderer.createContainer(
/* root: */ null,
ConcurrentRoot,
+ false,
null,
false,
'',
@@ -135,6 +136,7 @@ describe('ReactFiberHostContext', () => {
const container = Renderer.createContainer(
rootContext,
ConcurrentRoot,
+ false,
null,
false,
'',
diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js
index 76911d701de79..e850086439a67 100644
--- a/packages/react-test-renderer/src/ReactTestRenderer.js
+++ b/packages/react-test-renderer/src/ReactTestRenderer.js
@@ -473,6 +473,7 @@ function create(element: React$Element, options: TestRendererOptions) {
let root: FiberRoot | null = createContainer(
container,
isConcurrent ? ConcurrentRoot : LegacyRoot,
+ false,
null,
isStrictMode,
concurrentUpdatesByDefault,