diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index 9d1b6a16c203..44cdf4f240a2 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 4b67a7a8ebe9..3c18ce408309 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 f3ff64daeec4..3584715f91e2 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 df693b878499..9d6a38188376 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 3b751405a303..af0e35e128bd 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 33074054f1fe..c7820a703a0b 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 f8b8231f8f40..e2974586ec6a 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 744f5dfda9d9..9de82a99a7be 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 127b20fd5dac..e08a98653fb6 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 1dca2cd7a1d9..e751195dda00 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 e0ba72076a1a..8e4050dcfa33 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 4bae5d0b7b98..6fee8d948ebe 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 583539dd08b5..fc4912e7ec39 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 7a941dc15f40..fb6040109518 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 e4b2c3a5387a..e962039d9ca9 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 2a44cf94a14a..bea984c19f1c 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 f02a20222d0f..ef3d4f7979f2 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 8607b227e9b4..136276637d3e 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 4970b685b1c1..e014519320a5 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 00dd694be4f5..7ff03ceead0e 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 1e561e49facb..179b9c17ae41 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 000000000000..caadb978f69d --- /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 7223ad7d052b..558440effa77 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 d8bb6b16e29f..c1c090d82b0b 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 dd2e09c03b21..1fa3d4b6680d 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 d0c3d5b236ea..82e23de9965d 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 e850086439a6..76911d701de7 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,