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

* Pass children to hydration root constructor

I already made this change for the concurrent root API in #23309. This
does the same thing for the legacy API.

Doesn't change any behavior, but I will use this in the next steps.

* Add isRootDehydrated function

Currently this does nothing except read a boolean field, but I'm about
to change this logic.

Since this is accessed by React DOM, too, I put the function in a
separate module that can be deep imported. Previously, it was accessing
the FiberRoot directly. The reason it's a separate module is to break a
circular dependency between React DOM and the reconciler.

* Allow updates at lower pri without forcing client render

Currently, if a root is updated before the shell has finished hydrating
(for example, due to a top-level navigation), we immediately revert to
client rendering. This is rare because the root is expected is finish
quickly, but not exceedingly rare because the root may be suspended.

This adds support for updating the root without forcing a client render
as long as the update has lower priority than the initial hydration,
i.e. if the update is wrapped in startTransition.

To implement this, I had to do some refactoring. The main idea here is
to make it closer to how we implement hydration in Suspense boundaries:

- I moved isDehydrated from the shared FiberRoot object to the
HostRoot's state object.
- In the begin phase, I check if the root has received an by comparing
the new children to the initial children. If they are different, we
revert to client rendering, and set isDehydrated to false using a
derived state update (a la getDerivedStateFromProps).
- There are a few places where we used to set root.isDehydrated to false
as a way to force a client render. Instead, I set the ForceClientRender
flag on the root work-in-progress fiber.
- Whenever we fall back to client rendering, I log a recoverable error.

The overall code structure is almost identical to the corresponding
logic for Suspense components.

The reason this works is because if the update has lower priority than
the initial hydration, it won't be processed during the hydration
render, so the children will be the same.

We can go even further and allow updates at _higher_ priority (though
not sync) by implementing selective hydration at the root, like we do
for Suspense boundaries: interrupt the current render, attempt hydration
at slightly higher priority than the update, then continue rendering the
update. I haven't implemented this yet, but I've structured the code in
anticipation of adding this later.

* Wrap useMutableSource logic in feature flag
This commit is contained in:
Andrew Clark 2022-03-20 16:18:51 -04:00 committed by GitHub
parent dbe9e732af
commit 2e0d86d221
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 571 additions and 302 deletions

View File

@ -69,7 +69,6 @@ class Surface extends React.Component {
this._mountNode = createContainer(
this._surface,
LegacyRoot,
false,
null,
false,
false,

View File

@ -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);

View File

@ -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 <Text text="Initial" />;
}
// Server render
await resolveText('Initial');
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
expect(Scheduler).toHaveYielded(['Initial']);
await clientAct(async () => {
const root = ReactDOMClient.hydrateRoot(container, <App />);
// This has lower priority than the initial hydration, so the update
// won't be processed until after hydration finishes.
startTransition(() => {
root.render(<Text text="Updated" />);
});
});
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 <AsyncText text="Shell" />;
}
@ -245,9 +277,9 @@ describe('ReactDOMFizzShellHydration', () => {
root.render(<Text text="New screen" />);
});
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');
});

View File

@ -253,6 +253,15 @@ describe('ReactDOMRoot', () => {
);
});
it('callback passed to legacy hydrate() API', () => {
container.innerHTML = '<div>Hi</div>';
ReactDOM.hydrate(<div>Hi</div>, 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(<div>Hi</div>);

View File

@ -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<any, any>,
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);
}
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);
// Initial mount should not be batched.
flushSync(() => {
updateContainer(initialChildren, root, parentComponent, callback);
});
return root;
}
const root = createContainer(
container,
LegacyRoot,
forceHydrate,
null, // hydrationCallbacks
false, // isStrictMode
false, // concurrentUpdatesByDefaultOverride,
'', // identifierPrefix
noopOnRecoverableError, // onRecoverableError
null, // transitionCallbacks
);
markContainerAsRoot(root.current, container);
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);
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(

View File

@ -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,

View File

@ -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);

View File

@ -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.

View File

@ -215,7 +215,6 @@ function render(
root = createContainer(
containerTag,
concurrentRoot ? ConcurrentRoot : LegacyRoot,
false,
null,
false,
null,

View File

@ -211,7 +211,6 @@ function render(
root = createContainer(
containerTag,
LegacyRoot,
false,
null,
false,
null,

View File

@ -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,

View File

@ -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 && prevState.isDehydrated) {
// This is a hydration root whose shell has not yet hydrated. We should
// attempt to hydrate.
if (supportsHydration) {
const mutableSourceEagerHydrationData =
root.mutableSourceEagerHydrationData;
if (mutableSourceEagerHydrationData != null) {
for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
const mutableSource = ((mutableSourceEagerHydrationData[
i
]: any): MutableSource<any>);
const version = mutableSourceEagerHydrationData[i + 1];
setWorkInProgressVersion(mutableSource, version);
// 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<RootState> = (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<any>);
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,

View File

@ -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 && prevState.isDehydrated) {
// This is a hydration root whose shell has not yet hydrated. We should
// attempt to hydrate.
if (supportsHydration) {
const mutableSourceEagerHydrationData =
root.mutableSourceEagerHydrationData;
if (mutableSourceEagerHydrationData != null) {
for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
const mutableSource = ((mutableSourceEagerHydrationData[
i
]: any): MutableSource<any>);
const version = mutableSourceEagerHydrationData[i + 1];
setWorkInProgressVersion(mutableSource, version);
// 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<RootState> = (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<any>);
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,

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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<

View File

@ -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,
'',

View File

@ -473,7 +473,6 @@ function create(element: React$Element<any>, options: TestRendererOptions) {
let root: FiberRoot | null = createContainer(
container,
isConcurrent ? ConcurrentRoot : LegacyRoot,
false,
null,
isStrictMode,
concurrentUpdatesByDefault,