diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js
index 9d1b6a16c2038..44cdf4f240a21 100644
--- a/packages/react-art/src/ReactART.js
+++ b/packages/react-art/src/ReactART.js
@@ -69,7 +69,6 @@ class Surface extends React.Component {
this._mountNode = createContainer(
this._surface,
LegacyRoot,
- false,
null,
false,
false,
diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js
index 4b67a7a8ebe9c..3c18ce4083099 100644
--- a/packages/react-devtools-shared/src/backend/renderer.js
+++ b/packages/react-devtools-shared/src/backend/renderer.js
@@ -2704,9 +2704,14 @@ export function attach(
// TODO: relying on this seems a bit fishy.
const wasMounted =
alternate.memoizedState != null &&
- alternate.memoizedState.element != null;
+ alternate.memoizedState.element != null &&
+ // A dehydrated root is not considered mounted
+ alternate.memoizedState.isDehydrated !== true;
const isMounted =
- current.memoizedState != null && current.memoizedState.element != null;
+ current.memoizedState != null &&
+ current.memoizedState.element != null &&
+ // A dehydrated root is not considered mounted
+ current.memoizedState.isDehydrated !== true;
if (!wasMounted && isMounted) {
// Mount a new root.
setRootPseudoKey(currentRootID, current);
diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
index f3ff64daeec43..3584715f91e2f 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-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
index df693b8784992..9d6a38188376d 100644
--- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
@@ -253,6 +253,15 @@ 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 3b751405a3034..af0e35e128bd4 100644
--- a/packages/react-dom/src/client/ReactDOMLegacy.js
+++ b/packages/react-dom/src/client/ReactDOMLegacy.js
@@ -27,6 +27,7 @@ import {
import {
createContainer,
+ createHydrationContainer,
findHostInstanceWithNoPortals,
updateContainer,
flushSync,
@@ -109,34 +110,81 @@ function noopOnRecoverableError() {
function legacyCreateRootFromDOMContainer(
container: Container,
- forceHydrate: boolean,
+ initialChildren: ReactNodeList,
+ parentComponent: ?React$Component,
+ callback: ?Function,
+ isHydrationContainer: boolean,
): FiberRoot {
- // First clear any existing content.
- if (!forceHydrate) {
+ 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.
let rootSibling;
while ((rootSibling = container.lastChild)) {
container.removeChild(rootSibling);
}
- }
- const root = createContainer(
- container,
- LegacyRoot,
- forceHydrate,
- null, // hydrationCallbacks
- false, // isStrictMode
- false, // concurrentUpdatesByDefaultOverride,
- '', // identifierPrefix
- noopOnRecoverableError, // onRecoverableError
- null, // transitionCallbacks
- );
- markContainerAsRoot(root.current, container);
+ 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 rootContainerElement =
- container.nodeType === COMMENT_NODE ? container.parentNode : container;
- listenToAllSupportedEvents(rootContainerElement);
+ // Initial mount should not be batched.
+ flushSync(() => {
+ updateContainer(initialChildren, root, parentComponent, callback);
+ });
- return root;
+ return root;
+ }
}
function warnOnInvalidCallback(callback: mixed, callerName: string): void {
@@ -164,39 +212,30 @@ function legacyRenderSubtreeIntoContainer(
warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
}
- let root = container._reactRootContainer;
- let fiberRoot: FiberRoot;
- if (!root) {
+ const maybeRoot = container._reactRootContainer;
+ let root: FiberRoot;
+ if (!maybeRoot) {
// Initial mount
- root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
+ root = 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 {
- fiberRoot = root;
+ root = maybeRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
- const instance = getPublicRootInstance(fiberRoot);
+ const instance = getPublicRootInstance(root);
originalCallback.call(instance);
};
}
// Update
- updateContainer(children, fiberRoot, parentComponent, callback);
+ updateContainer(children, root, parentComponent, callback);
}
- return getPublicRootInstance(fiberRoot);
+ return getPublicRootInstance(root);
}
export function findDOMNode(
diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js
index 33074054f1fe5..c7820a703a0b0 100644
--- a/packages/react-dom/src/client/ReactDOMRoot.js
+++ b/packages/react-dom/src/client/ReactDOMRoot.js
@@ -224,7 +224,6 @@ export function createRoot(
const root = createContainer(
container,
ConcurrentRoot,
- false,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -303,6 +302,7 @@ 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 f8b8231f8f40e..e2974586ec6ac 100644
--- a/packages/react-dom/src/events/ReactDOMEventListener.js
+++ b/packages/react-dom/src/events/ReactDOMEventListener.js
@@ -53,6 +53,7 @@ import {
setCurrentUpdatePriority,
} from 'react-reconciler/src/ReactEventPriorities';
import ReactSharedInternals from 'shared/ReactSharedInternals';
+import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration';
const {ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -386,7 +387,7 @@ export function findInstanceBlockingEvent(
targetInst = null;
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
- if (root.isDehydrated) {
+ if (isRootDehydrated(root)) {
// 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 744f5dfda9d9b..9de82a99a7be3 100644
--- a/packages/react-dom/src/events/ReactDOMEventReplaying.js
+++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js
@@ -39,6 +39,7 @@ 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;
@@ -414,7 +415,7 @@ function attemptExplicitHydrationTarget(
}
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
- if (root.isDehydrated) {
+ if (isRootDehydrated(root)) {
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 127b20fd5dacf..e08a98653fb6c 100644
--- a/packages/react-native-renderer/src/ReactFabric.js
+++ b/packages/react-native-renderer/src/ReactFabric.js
@@ -215,7 +215,6 @@ 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 1dca2cd7a1d93..e751195dda00a 100644
--- a/packages/react-native-renderer/src/ReactNativeRenderer.js
+++ b/packages/react-native-renderer/src/ReactNativeRenderer.js
@@ -211,7 +211,6 @@ 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 e0ba72076a1af..8e4050dcfa336 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -974,7 +974,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
root = NoopRenderer.createContainer(
container,
tag,
- false,
null,
null,
false,
@@ -996,7 +995,6 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
const fiberRoot = NoopRenderer.createContainer(
container,
ConcurrentRoot,
- false,
null,
null,
false,
@@ -1029,7 +1027,6 @@ 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 4bae5d0b7b982..6fee8d948ebe2 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,9 +33,11 @@ import type {
SpawnedCachePool,
} from './ReactFiberCacheComponent.new';
import type {UpdateQueue} from './ReactUpdateQueue.new';
+import type {RootState} from './ReactFiberRoot.new';
import {
enableSuspenseAvoidThisFallback,
enableCPUSuspense,
+ enableUseMutableSource,
} from 'shared/ReactFeatureFlags';
import checkPropTypes from 'shared/checkPropTypes';
@@ -1311,14 +1317,9 @@ function pushHostRootContext(workInProgress) {
function updateHostRoot(current, workInProgress, renderLanes) {
pushHostRootContext(workInProgress);
- const updateQueue = workInProgress.updateQueue;
- if (current === null || updateQueue === null) {
- throw new Error(
- 'If the root does not have an updateQueue, we should have already ' +
- 'bailed out. This error is likely caused by a bug in React. Please ' +
- 'file an issue.',
- );
+ if (current === null) {
+ throw new Error('Should have a current fiber. This is a bug in React.');
}
const nextProps = workInProgress.pendingProps;
@@ -1326,8 +1327,8 @@ 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;
if (enableCache) {
@@ -1341,64 +1342,127 @@ 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 (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);
+ if (supportsHydration && prevState.isDehydrated) {
+ // This is a hydration root whose shell has not yet hydrated. We should
+ // attempt to hydrate.
+
+ // 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,
+ };
+ const updateQueue: UpdateQueue = (workInProgress.updateQueue: any);
+ // `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;
+
+ 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,
+ 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,
+ nextChildren,
+ renderLanes,
+ recoverableError,
+ );
+ } else {
+ // The outermost shell has not hydrated yet. Start hydrating.
+ enterHydrationState(workInProgress);
+ if (enableUseMutableSource && 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,
+ nextChildren: ReactNodeList,
+ renderLanes: Lanes,
+ recoverableError: Error,
+) {
+ // Revert to client rendering.
+ resetHydrationState();
+
+ queueHydrationError(recoverableError);
+
+ workInProgress.flags |= ForceClientRender;
+
+ 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 583539dd08b52..fc4912e7ec393 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,9 +33,11 @@ import type {
SpawnedCachePool,
} from './ReactFiberCacheComponent.old';
import type {UpdateQueue} from './ReactUpdateQueue.old';
+import type {RootState} from './ReactFiberRoot.old';
import {
enableSuspenseAvoidThisFallback,
enableCPUSuspense,
+ enableUseMutableSource,
} from 'shared/ReactFeatureFlags';
import checkPropTypes from 'shared/checkPropTypes';
@@ -1311,14 +1317,9 @@ function pushHostRootContext(workInProgress) {
function updateHostRoot(current, workInProgress, renderLanes) {
pushHostRootContext(workInProgress);
- const updateQueue = workInProgress.updateQueue;
- if (current === null || updateQueue === null) {
- throw new Error(
- 'If the root does not have an updateQueue, we should have already ' +
- 'bailed out. This error is likely caused by a bug in React. Please ' +
- 'file an issue.',
- );
+ if (current === null) {
+ throw new Error('Should have a current fiber. This is a bug in React.');
}
const nextProps = workInProgress.pendingProps;
@@ -1326,8 +1327,8 @@ 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;
if (enableCache) {
@@ -1341,64 +1342,127 @@ 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 (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);
+ if (supportsHydration && prevState.isDehydrated) {
+ // This is a hydration root whose shell has not yet hydrated. We should
+ // attempt to hydrate.
+
+ // 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,
+ };
+ const updateQueue: UpdateQueue = (workInProgress.updateQueue: any);
+ // `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;
+
+ 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,
+ 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,
+ nextChildren,
+ renderLanes,
+ recoverableError,
+ );
+ } else {
+ // The outermost shell has not hydrated yet. Start hydrating.
+ enterHydrationState(workInProgress);
+ if (enableUseMutableSource && 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,
+ nextChildren: ReactNodeList,
+ renderLanes: Lanes,
+ recoverableError: Error,
+) {
+ // Revert to client rendering.
+ resetHydrationState();
+
+ queueHydrationError(recoverableError);
+
+ workInProgress.flags |= ForceClientRender;
+
+ 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 7a941dc15f401..fb60401095184 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,
@@ -1869,11 +1870,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- const root: FiberRoot = finishedWork.stateNode;
- if (root.isDehydrated) {
- // 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;
@@ -1977,11 +1979,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- const root: FiberRoot = finishedWork.stateNode;
- if (root.isDehydrated) {
- // 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 e4b2c3a5387a1..e962039d9ca9c 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,
@@ -1869,11 +1870,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- const root: FiberRoot = finishedWork.stateNode;
- if (root.isDehydrated) {
- // 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;
@@ -1977,11 +1979,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
}
case HostRoot: {
if (supportsHydration) {
- const root: FiberRoot = finishedWork.stateNode;
- if (root.isDehydrated) {
- // 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 2a44cf94a14aa..bea984c19f1ce 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,
@@ -890,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 (!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;
+ } 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 f02a20222d0fe..ef3d4f7979f29 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,
@@ -890,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 (!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;
+ } 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 8607b227e9b40..136276637d3ea 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.new.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js
@@ -48,6 +48,7 @@ import {
isContextProvider as isLegacyContextProvider,
} from './ReactFiberContext.new';
import {createFiberRoot} from './ReactFiberRoot.new';
+import {isRootDehydrated} from './ReactFiberShellHydration';
import {
injectInternals,
markRenderScheduled,
@@ -245,9 +246,6 @@ 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,
@@ -255,10 +253,13 @@ 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,
@@ -270,6 +271,8 @@ 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,
@@ -284,6 +287,7 @@ export function createHydrationContainer(
containerInfo,
tag,
hydrate,
+ initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -298,13 +302,15 @@ export function createHydrationContainer(
// Schedule the initial render. In a hydration root, this is different from
// a regular update because the initial render must match was was rendered
// on the server.
+ // NOTE: This update intentionally doesn't have a payload. We're only using
+ // the update to schedule work on the root fiber (and, for legacy roots, to
+ // enqueue the callback if one is provided).
const current = root.current;
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.callback =
+ callback !== undefined && callback !== null ? callback : null;
enqueueUpdate(current, update, lane);
scheduleInitialHydrationOnRoot(root, lane, eventTime);
@@ -409,7 +415,7 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
switch (fiber.tag) {
case HostRoot:
const root: FiberRoot = fiber.stateNode;
- if (root.isDehydrated) {
+ if (isRootDehydrated(root)) {
// 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 4970b685b1c1a..e014519320a51 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.old.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js
@@ -48,6 +48,7 @@ import {
isContextProvider as isLegacyContextProvider,
} from './ReactFiberContext.old';
import {createFiberRoot} from './ReactFiberRoot.old';
+import {isRootDehydrated} from './ReactFiberShellHydration';
import {
injectInternals,
markRenderScheduled,
@@ -245,9 +246,6 @@ 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,
@@ -255,10 +253,13 @@ 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,
@@ -270,6 +271,8 @@ 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,
@@ -284,6 +287,7 @@ export function createHydrationContainer(
containerInfo,
tag,
hydrate,
+ initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
@@ -298,13 +302,15 @@ export function createHydrationContainer(
// Schedule the initial render. In a hydration root, this is different from
// a regular update because the initial render must match was was rendered
// on the server.
+ // NOTE: This update intentionally doesn't have a payload. We're only using
+ // the update to schedule work on the root fiber (and, for legacy roots, to
+ // enqueue the callback if one is provided).
const current = root.current;
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.callback =
+ callback !== undefined && callback !== null ? callback : null;
enqueueUpdate(current, update, lane);
scheduleInitialHydrationOnRoot(root, lane, eventTime);
@@ -409,7 +415,7 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
switch (fiber.tag) {
case HostRoot:
const root: FiberRoot = fiber.stateNode;
- if (root.isDehydrated) {
+ if (isRootDehydrated(root)) {
// 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 00dd694be4f5f..7ff03ceead0e3 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 1e561e49facb3..179b9c17ae416 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
new file mode 100644
index 0000000000000..caadb978f69d0
--- /dev/null
+++ b/packages/react-reconciler/src/ReactFiberShellHydration.js
@@ -0,0 +1,19 @@
+/**
+ * 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 7223ad7d052b0..558440effa77a 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -88,6 +88,7 @@ import {
createWorkInProgress,
assignFiberPropertiesInDEV,
} from './ReactFiber.new';
+import {isRootDehydrated} from './ReactFiberShellHydration';
import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode';
import {
HostRoot,
@@ -109,6 +110,7 @@ import {
StoreConsistency,
HostEffectMask,
Hydrating,
+ ForceClientRender,
BeforeMutationMask,
MutationMask,
LayoutMask,
@@ -581,34 +583,7 @@ export function scheduleUpdateOnFiber(
}
}
- 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) {
+ if (root === workInProgressRoot) {
// TODO: Consolidate with `isInterleavedUpdate` check
// Received an update to a tree that's in the middle of rendering. Mark
@@ -1016,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.
- if (root.isDehydrated) {
- root.isDehydrated = false;
+
+ // 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 (__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.
@@ -1453,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;
@@ -1479,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;
@@ -1495,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 d8bb6b16e29fb..c1c090d82b0b5 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -88,6 +88,7 @@ import {
createWorkInProgress,
assignFiberPropertiesInDEV,
} from './ReactFiber.old';
+import {isRootDehydrated} from './ReactFiberShellHydration';
import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode';
import {
HostRoot,
@@ -109,6 +110,7 @@ import {
StoreConsistency,
HostEffectMask,
Hydrating,
+ ForceClientRender,
BeforeMutationMask,
MutationMask,
LayoutMask,
@@ -581,34 +583,7 @@ export function scheduleUpdateOnFiber(
}
}
- 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) {
+ if (root === workInProgressRoot) {
// TODO: Consolidate with `isInterleavedUpdate` check
// Received an update to a tree that's in the middle of rendering. Mark
@@ -1016,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.
- if (root.isDehydrated) {
- root.isDehydrated = false;
+
+ // 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 (__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.
@@ -1453,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;
@@ -1479,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;
@@ -1495,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 dd2e09c03b210..1fa3d4b6680d1 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<
diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
index d0c3d5b236ea4..82e23de9965da 100644
--- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
@@ -72,7 +72,6 @@ describe('ReactFiberHostContext', () => {
const container = Renderer.createContainer(
/* root: */ null,
ConcurrentRoot,
- false,
null,
false,
'',
@@ -136,7 +135,6 @@ 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 e850086439a67..76911d701de79 100644
--- a/packages/react-test-renderer/src/ReactTestRenderer.js
+++ b/packages/react-test-renderer/src/ReactTestRenderer.js
@@ -473,7 +473,6 @@ function create(element: React$Element, options: TestRendererOptions) {
let root: FiberRoot | null = createContainer(
container,
isConcurrent ? ConcurrentRoot : LegacyRoot,
- false,
null,
isStrictMode,
concurrentUpdatesByDefault,