From 832e2987e01aa357c3b2e551acae0682ca36fb14 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 11 Mar 2022 21:31:23 -0500 Subject: [PATCH] Revert accdientally merged PR (#24081) --- packages/react-art/src/ReactART.js | 1 + .../ReactDOMFizzShellHydration-test.js | 36 +--- .../src/__tests__/ReactDOMRoot-test.js | 9 - .../react-dom/src/client/ReactDOMLegacy.js | 119 +++++-------- packages/react-dom/src/client/ReactDOMRoot.js | 2 +- .../src/events/ReactDOMEventListener.js | 3 +- .../src/events/ReactDOMEventReplaying.js | 3 +- .../react-native-renderer/src/ReactFabric.js | 1 + .../src/ReactNativeRenderer.js | 1 + .../src/createReactNoop.js | 3 + .../src/ReactFiberBeginWork.new.js | 159 +++++------------- .../src/ReactFiberBeginWork.old.js | 159 +++++------------- .../src/ReactFiberCommitWork.new.js | 23 ++- .../src/ReactFiberCommitWork.old.js | 23 ++- .../src/ReactFiberCompleteWork.new.js | 30 +--- .../src/ReactFiberCompleteWork.old.js | 30 +--- .../src/ReactFiberReconciler.new.js | 18 +- .../src/ReactFiberReconciler.old.js | 18 +- .../src/ReactFiberRoot.new.js | 14 +- .../src/ReactFiberRoot.old.js | 14 +- .../src/ReactFiberShellHydration.js | 19 --- .../src/ReactFiberWorkLoop.new.js | 80 +++++---- .../src/ReactFiberWorkLoop.old.js | 80 +++++---- .../src/ReactInternalTypes.js | 2 + .../ReactFiberHostContext-test.internal.js | 2 + .../src/ReactTestRenderer.js | 1 + 26 files changed, 288 insertions(+), 562 deletions(-) delete mode 100644 packages/react-reconciler/src/ReactFiberShellHydration.js diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index 44cdf4f240a21..9d1b6a16c2038 100644 --- a/packages/react-art/src/ReactART.js +++ b/packages/react-art/src/ReactART.js @@ -69,6 +69,7 @@ class Surface extends React.Component { this._mountNode = createContainer( this._surface, LegacyRoot, + false, null, false, false, diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js index 3584715f91e2f..f3ff64daeec43 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js @@ -9,7 +9,6 @@ let JSDOM; let React; -let startTransition; let ReactDOMClient; let Scheduler; let clientAct; @@ -34,8 +33,6 @@ describe('ReactDOMFizzShellHydration', () => { ReactDOMFizzServer = require('react-dom/server'); Stream = require('stream'); - startTransition = React.startTransition; - textCache = new Map(); // Test Environment @@ -217,36 +214,7 @@ describe('ReactDOMFizzShellHydration', () => { expect(container.textContent).toBe('Shell'); }); - test( - 'updating the root at lower priority than initial hydration does not ' + - 'force a client render', - async () => { - function App() { - return ; - } - - // Server render - await resolveText('Initial'); - await serverAct(async () => { - const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); - pipe(writable); - }); - expect(Scheduler).toHaveYielded(['Initial']); - - await clientAct(async () => { - const root = ReactDOMClient.hydrateRoot(container, ); - // This has lower priority than the initial hydration, so the update - // won't be processed until after hydration finishes. - startTransition(() => { - root.render(); - }); - }); - expect(Scheduler).toHaveYielded(['Initial', 'Updated']); - expect(container.textContent).toBe('Updated'); - }, - ); - - test('updating the root while the shell is suspended forces a client render', async () => { + test('updating the root before the shell hydrates forces a client render', async () => { function App() { return ; } @@ -277,9 +245,9 @@ describe('ReactDOMFizzShellHydration', () => { root.render(); }); expect(Scheduler).toHaveYielded([ - 'New screen', 'This root received an early update, before anything was able ' + 'hydrate. Switched the entire root to client rendering.', + 'New screen', ]); expect(container.textContent).toBe('New screen'); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js index 9d6a38188376d..df693b8784992 100644 --- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js @@ -253,15 +253,6 @@ describe('ReactDOMRoot', () => { ); }); - it('callback passed to legacy hydrate() API', () => { - container.innerHTML = '
Hi
'; - ReactDOM.hydrate(
Hi
, container, () => { - Scheduler.unstable_yieldValue('callback'); - }); - expect(container.textContent).toEqual('Hi'); - expect(Scheduler).toHaveYielded(['callback']); - }); - it('warns when unmounting with legacy API (no previous content)', () => { const root = ReactDOMClient.createRoot(container); root.render(
Hi
); diff --git a/packages/react-dom/src/client/ReactDOMLegacy.js b/packages/react-dom/src/client/ReactDOMLegacy.js index af0e35e128bd4..3b751405a3034 100644 --- a/packages/react-dom/src/client/ReactDOMLegacy.js +++ b/packages/react-dom/src/client/ReactDOMLegacy.js @@ -27,7 +27,6 @@ import { import { createContainer, - createHydrationContainer, findHostInstanceWithNoPortals, updateContainer, flushSync, @@ -110,81 +109,34 @@ function noopOnRecoverableError() { function legacyCreateRootFromDOMContainer( container: Container, - initialChildren: ReactNodeList, - parentComponent: ?React$Component, - callback: ?Function, - isHydrationContainer: boolean, + forceHydrate: boolean, ): FiberRoot { - if (isHydrationContainer) { - if (typeof callback === 'function') { - const originalCallback = callback; - callback = function() { - const instance = getPublicRootInstance(root); - originalCallback.call(instance); - }; - } - - const root = createHydrationContainer( - initialChildren, - callback, - container, - LegacyRoot, - null, // hydrationCallbacks - false, // isStrictMode - false, // concurrentUpdatesByDefaultOverride, - '', // identifierPrefix - noopOnRecoverableError, - // TODO(luna) Support hydration later - null, - ); - container._reactRootContainer = root; - markContainerAsRoot(root.current, container); - - const rootContainerElement = - container.nodeType === COMMENT_NODE ? container.parentNode : container; - listenToAllSupportedEvents(rootContainerElement); - - flushSync(); - return root; - } else { - // First clear any existing content. + // First clear any existing content. + if (!forceHydrate) { let rootSibling; while ((rootSibling = container.lastChild)) { container.removeChild(rootSibling); } + } - if (typeof callback === 'function') { - const originalCallback = callback; - callback = function() { - const instance = getPublicRootInstance(root); - originalCallback.call(instance); - }; - } - - const root = createContainer( - container, - LegacyRoot, - null, // hydrationCallbacks - false, // isStrictMode - false, // concurrentUpdatesByDefaultOverride, - '', // identifierPrefix - noopOnRecoverableError, // onRecoverableError - null, // transitionCallbacks - ); - container._reactRootContainer = root; - markContainerAsRoot(root.current, container); - - const rootContainerElement = - container.nodeType === COMMENT_NODE ? container.parentNode : container; - listenToAllSupportedEvents(rootContainerElement); + const root = createContainer( + container, + LegacyRoot, + forceHydrate, + null, // hydrationCallbacks + false, // isStrictMode + false, // concurrentUpdatesByDefaultOverride, + '', // identifierPrefix + noopOnRecoverableError, // onRecoverableError + null, // transitionCallbacks + ); + markContainerAsRoot(root.current, container); - // Initial mount should not be batched. - flushSync(() => { - updateContainer(initialChildren, root, parentComponent, callback); - }); + const rootContainerElement = + container.nodeType === COMMENT_NODE ? container.parentNode : container; + listenToAllSupportedEvents(rootContainerElement); - return root; - } + return root; } function warnOnInvalidCallback(callback: mixed, callerName: string): void { @@ -212,30 +164,39 @@ function legacyRenderSubtreeIntoContainer( warnOnInvalidCallback(callback === undefined ? null : callback, 'render'); } - const maybeRoot = container._reactRootContainer; - let root: FiberRoot; - if (!maybeRoot) { + let root = container._reactRootContainer; + let fiberRoot: FiberRoot; + if (!root) { // Initial mount - root = legacyCreateRootFromDOMContainer( + root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, - children, - parentComponent, - callback, forceHydrate, ); + fiberRoot = root; + if (typeof callback === 'function') { + const originalCallback = callback; + callback = function() { + const instance = getPublicRootInstance(fiberRoot); + originalCallback.call(instance); + }; + } + // Initial mount should not be batched. + flushSync(() => { + updateContainer(children, fiberRoot, parentComponent, callback); + }); } else { - root = maybeRoot; + fiberRoot = root; if (typeof callback === 'function') { const originalCallback = callback; callback = function() { - const instance = getPublicRootInstance(root); + const instance = getPublicRootInstance(fiberRoot); originalCallback.call(instance); }; } // Update - updateContainer(children, root, parentComponent, callback); + updateContainer(children, fiberRoot, parentComponent, callback); } - return getPublicRootInstance(root); + return getPublicRootInstance(fiberRoot); } export function findDOMNode( diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index 6e7192ae10453..d71de3bb0c26d 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -225,6 +225,7 @@ export function createRoot( const root = createContainer( container, ConcurrentRoot, + false, null, isStrictMode, concurrentUpdatesByDefaultOverride, @@ -301,7 +302,6 @@ export function hydrateRoot( const root = createHydrationContainer( initialChildren, - null, container, ConcurrentRoot, hydrationCallbacks, diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index e2974586ec6ac..f8b8231f8f40e 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -53,7 +53,6 @@ import { setCurrentUpdatePriority, } from 'react-reconciler/src/ReactEventPriorities'; import ReactSharedInternals from 'shared/ReactSharedInternals'; -import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration'; const {ReactCurrentBatchConfig} = ReactSharedInternals; @@ -387,7 +386,7 @@ export function findInstanceBlockingEvent( targetInst = null; } else if (tag === HostRoot) { const root: FiberRoot = nearestMounted.stateNode; - if (isRootDehydrated(root)) { + if (root.isDehydrated) { // If this happens during a replay something went wrong and it might block // the whole system. return getContainerFromFiber(nearestMounted); diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index 9de82a99a7be3..744f5dfda9d9b 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -39,7 +39,6 @@ import { } from '../client/ReactDOMComponentTree'; import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags'; import {isHigherEventPriority} from 'react-reconciler/src/ReactEventPriorities'; -import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration'; let _attemptSynchronousHydration: (fiber: Object) => void; @@ -415,7 +414,7 @@ function attemptExplicitHydrationTarget( } } else if (tag === HostRoot) { const root: FiberRoot = nearestMounted.stateNode; - if (isRootDehydrated(root)) { + if (root.isDehydrated) { queuedTarget.blockedOn = getContainerFromFiber(nearestMounted); // We don't currently have a way to increase the priority of // a root other than sync. diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index e08a98653fb6c..127b20fd5dacf 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -215,6 +215,7 @@ function render( root = createContainer( containerTag, concurrentRoot ? ConcurrentRoot : LegacyRoot, + false, null, false, null, diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index e751195dda00a..1dca2cd7a1d93 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -211,6 +211,7 @@ function render( root = createContainer( containerTag, LegacyRoot, + false, null, false, null, diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 8e4050dcfa336..e0ba72076a1af 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -974,6 +974,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { root = NoopRenderer.createContainer( container, tag, + false, null, null, false, @@ -995,6 +996,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { const fiberRoot = NoopRenderer.createContainer( container, ConcurrentRoot, + false, null, null, false, @@ -1027,6 +1029,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { const fiberRoot = NoopRenderer.createContainer( container, LegacyRoot, + false, null, null, false, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 7dc29bd2b0f08..4bae5d0b7b982 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -7,11 +7,7 @@ * @flow */ -import type { - ReactProviderType, - ReactContext, - ReactNodeList, -} from 'shared/ReactTypes'; +import type {ReactProviderType, ReactContext} from 'shared/ReactTypes'; import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {TypeOfMode} from './ReactTypeOfMode'; @@ -33,7 +29,6 @@ import type { SpawnedCachePool, } from './ReactFiberCacheComponent.new'; import type {UpdateQueue} from './ReactUpdateQueue.new'; -import type {RootState} from './ReactFiberRoot.new'; import { enableSuspenseAvoidThisFallback, enableCPUSuspense, @@ -1316,7 +1311,7 @@ function pushHostRootContext(workInProgress) { function updateHostRoot(current, workInProgress, renderLanes) { pushHostRootContext(workInProgress); - const updateQueue: UpdateQueue = (workInProgress.updateQueue: any); + const updateQueue = workInProgress.updateQueue; if (current === null || updateQueue === null) { throw new Error( @@ -1331,7 +1326,7 @@ function updateHostRoot(current, workInProgress, renderLanes) { const prevChildren = prevState.element; cloneUpdateQueue(current, workInProgress); processUpdateQueue(workInProgress, nextProps, null, renderLanes); - const nextState: RootState = workInProgress.memoizedState; + const nextState = workInProgress.memoizedState; const root: FiberRoot = workInProgress.stateNode; @@ -1346,130 +1341,64 @@ function updateHostRoot(current, workInProgress, renderLanes) { } if (enableTransitionTracing) { - // FIXME: Slipped past code review. This is not a safe mutation: - // workInProgress.memoizedState is a shared object. Need to fix before - // rolling out the Transition Tracing experiment. workInProgress.memoizedState.transitions = getWorkInProgressTransitions(); } // Caution: React DevTools currently depends on this property // being called "element". const nextChildren = nextState.element; - if (supportsHydration && prevState.isDehydrated) { - // This is a hydration root whose shell has not yet hydrated. We should - // attempt to hydrate. - if (workInProgress.flags & ForceClientRender) { - // Something errored during a previous attempt to hydrate the shell, so we - // forced a client render. - const recoverableError = new Error( - 'There was an error while hydrating. Because the error happened outside ' + - 'of a Suspense boundary, the entire root will switch to ' + - 'client rendering.', - ); - return mountHostRootWithoutHydrating( - current, - workInProgress, - updateQueue, - nextState, - nextChildren, - renderLanes, - recoverableError, - ); - } else if (nextChildren !== prevChildren) { - const recoverableError = new Error( - 'This root received an early update, before anything was able ' + - 'hydrate. Switched the entire root to client rendering.', - ); - return mountHostRootWithoutHydrating( - current, - workInProgress, - updateQueue, - nextState, - nextChildren, - renderLanes, - recoverableError, - ); - } else { - // The outermost shell has not hydrated yet. Start hydrating. - enterHydrationState(workInProgress); - if (supportsHydration) { - const mutableSourceEagerHydrationData = - root.mutableSourceEagerHydrationData; - if (mutableSourceEagerHydrationData != null) { - for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) { - const mutableSource = ((mutableSourceEagerHydrationData[ - i - ]: any): MutableSource); - const version = mutableSourceEagerHydrationData[i + 1]; - setWorkInProgressVersion(mutableSource, version); - } + if (nextChildren === prevChildren) { + resetHydrationState(); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); + } + if (root.isDehydrated && enterHydrationState(workInProgress)) { + // If we don't have any current children this might be the first pass. + // We always try to hydrate. If this isn't a hydration pass there won't + // be any children to hydrate which is effectively the same thing as + // not hydrating. + + if (supportsHydration) { + const mutableSourceEagerHydrationData = + root.mutableSourceEagerHydrationData; + if (mutableSourceEagerHydrationData != null) { + for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) { + const mutableSource = ((mutableSourceEagerHydrationData[ + i + ]: any): MutableSource); + const version = mutableSourceEagerHydrationData[i + 1]; + setWorkInProgressVersion(mutableSource, version); } } + } - const child = mountChildFibers( - workInProgress, - null, - nextChildren, - renderLanes, - ); - workInProgress.child = child; + const child = mountChildFibers( + workInProgress, + null, + nextChildren, + renderLanes, + ); + workInProgress.child = child; - let node = child; - while (node) { - // Mark each child as hydrating. This is a fast path to know whether this - // tree is part of a hydrating tree. This is used to determine if a child - // node has fully mounted yet, and for scheduling event replaying. - // Conceptually this is similar to Placement in that a new subtree is - // inserted into the React tree here. It just happens to not need DOM - // mutations because it already exists. - node.flags = (node.flags & ~Placement) | Hydrating; - node = node.sibling; - } + let node = child; + while (node) { + // Mark each child as hydrating. This is a fast path to know whether this + // tree is part of a hydrating tree. This is used to determine if a child + // node has fully mounted yet, and for scheduling event replaying. + // Conceptually this is similar to Placement in that a new subtree is + // inserted into the React tree here. It just happens to not need DOM + // mutations because it already exists. + node.flags = (node.flags & ~Placement) | Hydrating; + node = node.sibling; } } else { - // Root is not dehydrated. Either this is a client-only root, or it - // already hydrated. - resetHydrationState(); - if (nextChildren === prevChildren) { - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); - } + // Otherwise reset hydration state in case we aborted and resumed another + // root. reconcileChildren(current, workInProgress, nextChildren, renderLanes); + resetHydrationState(); } return workInProgress.child; } -function mountHostRootWithoutHydrating( - current: Fiber, - workInProgress: Fiber, - updateQueue: UpdateQueue, - nextState: RootState, - nextChildren: ReactNodeList, - renderLanes: Lanes, - recoverableError: Error, -) { - // Revert to client rendering. - resetHydrationState(); - - queueHydrationError(recoverableError); - - workInProgress.flags |= ForceClientRender; - - // Flip isDehydrated to false to indicate that when this render - // finishes, the root will no longer be dehydrated. - const overrideState: RootState = { - element: nextChildren, - isDehydrated: false, - cache: nextState.cache, - transitions: nextState.transitions, - }; - // `baseState` can always be the last state because the root doesn't - // have reducer functions so it doesn't need rebasing. - updateQueue.baseState = overrideState; - workInProgress.memoizedState = overrideState; - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - function updateHostComponent( current: Fiber | null, workInProgress: Fiber, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 206773b03b334..583539dd08b52 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -7,11 +7,7 @@ * @flow */ -import type { - ReactProviderType, - ReactContext, - ReactNodeList, -} from 'shared/ReactTypes'; +import type {ReactProviderType, ReactContext} from 'shared/ReactTypes'; import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {TypeOfMode} from './ReactTypeOfMode'; @@ -33,7 +29,6 @@ import type { SpawnedCachePool, } from './ReactFiberCacheComponent.old'; import type {UpdateQueue} from './ReactUpdateQueue.old'; -import type {RootState} from './ReactFiberRoot.old'; import { enableSuspenseAvoidThisFallback, enableCPUSuspense, @@ -1316,7 +1311,7 @@ function pushHostRootContext(workInProgress) { function updateHostRoot(current, workInProgress, renderLanes) { pushHostRootContext(workInProgress); - const updateQueue: UpdateQueue = (workInProgress.updateQueue: any); + const updateQueue = workInProgress.updateQueue; if (current === null || updateQueue === null) { throw new Error( @@ -1331,7 +1326,7 @@ function updateHostRoot(current, workInProgress, renderLanes) { const prevChildren = prevState.element; cloneUpdateQueue(current, workInProgress); processUpdateQueue(workInProgress, nextProps, null, renderLanes); - const nextState: RootState = workInProgress.memoizedState; + const nextState = workInProgress.memoizedState; const root: FiberRoot = workInProgress.stateNode; @@ -1346,130 +1341,64 @@ function updateHostRoot(current, workInProgress, renderLanes) { } if (enableTransitionTracing) { - // FIXME: Slipped past code review. This is not a safe mutation: - // workInProgress.memoizedState is a shared object. Need to fix before - // rolling out the Transition Tracing experiment. workInProgress.memoizedState.transitions = getWorkInProgressTransitions(); } // Caution: React DevTools currently depends on this property // being called "element". const nextChildren = nextState.element; - if (supportsHydration && prevState.isDehydrated) { - // This is a hydration root whose shell has not yet hydrated. We should - // attempt to hydrate. - if (workInProgress.flags & ForceClientRender) { - // Something errored during a previous attempt to hydrate the shell, so we - // forced a client render. - const recoverableError = new Error( - 'There was an error while hydrating. Because the error happened outside ' + - 'of a Suspense boundary, the entire root will switch to ' + - 'client rendering.', - ); - return mountHostRootWithoutHydrating( - current, - workInProgress, - updateQueue, - nextState, - nextChildren, - renderLanes, - recoverableError, - ); - } else if (nextChildren !== prevChildren) { - const recoverableError = new Error( - 'This root received an early update, before anything was able ' + - 'hydrate. Switched the entire root to client rendering.', - ); - return mountHostRootWithoutHydrating( - current, - workInProgress, - updateQueue, - nextState, - nextChildren, - renderLanes, - recoverableError, - ); - } else { - // The outermost shell has not hydrated yet. Start hydrating. - enterHydrationState(workInProgress); - if (supportsHydration) { - const mutableSourceEagerHydrationData = - root.mutableSourceEagerHydrationData; - if (mutableSourceEagerHydrationData != null) { - for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) { - const mutableSource = ((mutableSourceEagerHydrationData[ - i - ]: any): MutableSource); - const version = mutableSourceEagerHydrationData[i + 1]; - setWorkInProgressVersion(mutableSource, version); - } + if (nextChildren === prevChildren) { + resetHydrationState(); + return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); + } + if (root.isDehydrated && enterHydrationState(workInProgress)) { + // If we don't have any current children this might be the first pass. + // We always try to hydrate. If this isn't a hydration pass there won't + // be any children to hydrate which is effectively the same thing as + // not hydrating. + + if (supportsHydration) { + const mutableSourceEagerHydrationData = + root.mutableSourceEagerHydrationData; + if (mutableSourceEagerHydrationData != null) { + for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) { + const mutableSource = ((mutableSourceEagerHydrationData[ + i + ]: any): MutableSource); + const version = mutableSourceEagerHydrationData[i + 1]; + setWorkInProgressVersion(mutableSource, version); } } + } - const child = mountChildFibers( - workInProgress, - null, - nextChildren, - renderLanes, - ); - workInProgress.child = child; + const child = mountChildFibers( + workInProgress, + null, + nextChildren, + renderLanes, + ); + workInProgress.child = child; - let node = child; - while (node) { - // Mark each child as hydrating. This is a fast path to know whether this - // tree is part of a hydrating tree. This is used to determine if a child - // node has fully mounted yet, and for scheduling event replaying. - // Conceptually this is similar to Placement in that a new subtree is - // inserted into the React tree here. It just happens to not need DOM - // mutations because it already exists. - node.flags = (node.flags & ~Placement) | Hydrating; - node = node.sibling; - } + let node = child; + while (node) { + // Mark each child as hydrating. This is a fast path to know whether this + // tree is part of a hydrating tree. This is used to determine if a child + // node has fully mounted yet, and for scheduling event replaying. + // Conceptually this is similar to Placement in that a new subtree is + // inserted into the React tree here. It just happens to not need DOM + // mutations because it already exists. + node.flags = (node.flags & ~Placement) | Hydrating; + node = node.sibling; } } else { - // Root is not dehydrated. Either this is a client-only root, or it - // already hydrated. - resetHydrationState(); - if (nextChildren === prevChildren) { - return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); - } + // Otherwise reset hydration state in case we aborted and resumed another + // root. reconcileChildren(current, workInProgress, nextChildren, renderLanes); + resetHydrationState(); } return workInProgress.child; } -function mountHostRootWithoutHydrating( - current: Fiber, - workInProgress: Fiber, - updateQueue: UpdateQueue, - nextState: RootState, - nextChildren: ReactNodeList, - renderLanes: Lanes, - recoverableError: Error, -) { - // Revert to client rendering. - resetHydrationState(); - - queueHydrationError(recoverableError); - - workInProgress.flags |= ForceClientRender; - - // Flip isDehydrated to false to indicate that when this render - // finishes, the root will no longer be dehydrated. - const overrideState: RootState = { - element: nextChildren, - isDehydrated: false, - cache: nextState.cache, - transitions: nextState.transitions, - }; - // `baseState` can always be the last state because the root doesn't - // have reducer functions so it doesn't need rebasing. - updateQueue.baseState = overrideState; - workInProgress.memoizedState = overrideState; - reconcileChildren(current, workInProgress, nextChildren, renderLanes); - return workInProgress.child; -} - function updateHostComponent( current: Fiber | null, workInProgress: Fiber, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 776c402a00eb9..75553555b928d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -25,7 +25,6 @@ import type {Wakeable} from 'shared/ReactTypes'; import type {OffscreenState} from './ReactFiberOffscreenComponent'; import type {HookFlags} from './ReactHookEffectTags'; import type {Cache} from './ReactFiberCacheComponent.new'; -import type {RootState} from './ReactFiberRoot.new'; import { enableCreateEventHandleAPI, @@ -1878,12 +1877,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { } case HostRoot: { if (supportsHydration) { - if (current !== null) { - const prevRootState: RootState = current.memoizedState; - if (prevRootState.isDehydrated) { - const root: FiberRoot = finishedWork.stateNode; - commitHydratedContainer(root.containerInfo); - } + const root: FiberRoot = finishedWork.stateNode; + if (root.isDehydrated) { + // We've just hydrated. No need to hydrate again. + root.isDehydrated = false; + commitHydratedContainer(root.containerInfo); } } break; @@ -1987,12 +1985,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { } case HostRoot: { if (supportsHydration) { - if (current !== null) { - const prevRootState: RootState = current.memoizedState; - if (prevRootState.isDehydrated) { - const root: FiberRoot = finishedWork.stateNode; - commitHydratedContainer(root.containerInfo); - } + const root: FiberRoot = finishedWork.stateNode; + if (root.isDehydrated) { + // We've just hydrated. No need to hydrate again. + root.isDehydrated = false; + commitHydratedContainer(root.containerInfo); } } return; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index a53d4a2a87525..23e9d6070c9a2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -25,7 +25,6 @@ import type {Wakeable} from 'shared/ReactTypes'; import type {OffscreenState} from './ReactFiberOffscreenComponent'; import type {HookFlags} from './ReactHookEffectTags'; import type {Cache} from './ReactFiberCacheComponent.old'; -import type {RootState} from './ReactFiberRoot.old'; import { enableCreateEventHandleAPI, @@ -1878,12 +1877,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { } case HostRoot: { if (supportsHydration) { - if (current !== null) { - const prevRootState: RootState = current.memoizedState; - if (prevRootState.isDehydrated) { - const root: FiberRoot = finishedWork.stateNode; - commitHydratedContainer(root.containerInfo); - } + const root: FiberRoot = finishedWork.stateNode; + if (root.isDehydrated) { + // We've just hydrated. No need to hydrate again. + root.isDehydrated = false; + commitHydratedContainer(root.containerInfo); } } break; @@ -1987,12 +1985,11 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { } case HostRoot: { if (supportsHydration) { - if (current !== null) { - const prevRootState: RootState = current.memoizedState; - if (prevRootState.isDehydrated) { - const root: FiberRoot = finishedWork.stateNode; - commitHydratedContainer(root.containerInfo); - } + const root: FiberRoot = finishedWork.stateNode; + if (root.isDehydrated) { + // We've just hydrated. No need to hydrate again. + root.isDehydrated = false; + commitHydratedContainer(root.containerInfo); } } return; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index bea984c19f1ce..2a44cf94a14aa 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -8,7 +8,6 @@ */ import type {Fiber} from './ReactInternalTypes'; -import type {RootState} from './ReactFiberRoot.new'; import type {Lanes, Lane} from './ReactFiberLane.new'; import type { ReactScopeInstance, @@ -891,29 +890,12 @@ function completeWork( // If we hydrated, then we'll need to schedule an update for // the commit side-effects on the root. markUpdate(workInProgress); - } else { - if (current !== null) { - const prevState: RootState = current.memoizedState; - if ( - // Check if this is a client root - !prevState.isDehydrated || - // Check if we reverted to client rendering (e.g. due to an error) - (workInProgress.flags & ForceClientRender) !== NoFlags - ) { - // Schedule an effect to clear this container at the start of the - // next commit. This handles the case of React rendering into a - // container with previous children. It's also safe to do for - // updates too, because current.child would only be null if the - // previous render was null (so the container would already - // be empty). - workInProgress.flags |= Snapshot; - - // If this was a forced client render, there may have been - // recoverable errors during first hydration attempt. If so, add - // them to a queue so we can log them in the commit phase. - upgradeHydrationErrorsToRecoverable(); - } - } + } else if (!fiberRoot.isDehydrated) { + // Schedule an effect to clear this container at the start of the next commit. + // This handles the case of React rendering into a container with previous children. + // It's also safe to do for updates too, because current.child would only be null + // if the previous render was null (so the container would already be empty). + workInProgress.flags |= Snapshot; } } updateHostContainer(current, workInProgress); diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index ef3d4f7979f29..f02a20222d0fe 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -8,7 +8,6 @@ */ import type {Fiber} from './ReactInternalTypes'; -import type {RootState} from './ReactFiberRoot.old'; import type {Lanes, Lane} from './ReactFiberLane.old'; import type { ReactScopeInstance, @@ -891,29 +890,12 @@ function completeWork( // If we hydrated, then we'll need to schedule an update for // the commit side-effects on the root. markUpdate(workInProgress); - } else { - if (current !== null) { - const prevState: RootState = current.memoizedState; - if ( - // Check if this is a client root - !prevState.isDehydrated || - // Check if we reverted to client rendering (e.g. due to an error) - (workInProgress.flags & ForceClientRender) !== NoFlags - ) { - // Schedule an effect to clear this container at the start of the - // next commit. This handles the case of React rendering into a - // container with previous children. It's also safe to do for - // updates too, because current.child would only be null if the - // previous render was null (so the container would already - // be empty). - workInProgress.flags |= Snapshot; - - // If this was a forced client render, there may have been - // recoverable errors during first hydration attempt. If so, add - // them to a queue so we can log them in the commit phase. - upgradeHydrationErrorsToRecoverable(); - } - } + } else if (!fiberRoot.isDehydrated) { + // Schedule an effect to clear this container at the start of the next commit. + // This handles the case of React rendering into a container with previous children. + // It's also safe to do for updates too, because current.child would only be null + // if the previous render was null (so the container would already be empty). + workInProgress.flags |= Snapshot; } } updateHostContainer(current, workInProgress); diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 849470551a2bc..8607b227e9b40 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -48,7 +48,6 @@ import { isContextProvider as isLegacyContextProvider, } from './ReactFiberContext.new'; import {createFiberRoot} from './ReactFiberRoot.new'; -import {isRootDehydrated} from './ReactFiberShellHydration'; import { injectInternals, markRenderScheduled, @@ -246,6 +245,9 @@ function findHostInstanceWithWarning( export function createContainer( containerInfo: Container, tag: RootTag, + // TODO: We can remove hydration-specific stuff from createContainer once + // we delete legacy mode. The new root API uses createHydrationContainer. + hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, @@ -253,13 +255,10 @@ export function createContainer( onRecoverableError: (error: mixed) => void, transitionCallbacks: null | TransitionTracingCallbacks, ): OpaqueRoot { - const hydrate = false; - const initialChildren = null; return createFiberRoot( containerInfo, tag, hydrate, - initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, @@ -271,8 +270,6 @@ export function createContainer( export function createHydrationContainer( initialChildren: ReactNodeList, - // TODO: Remove `callback` when we delete legacy mode. - callback: ?Function, containerInfo: Container, tag: RootTag, hydrationCallbacks: null | SuspenseHydrationCallbacks, @@ -287,7 +284,6 @@ export function createHydrationContainer( containerInfo, tag, hydrate, - initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, @@ -306,9 +302,9 @@ export function createHydrationContainer( const eventTime = requestEventTime(); const lane = requestUpdateLane(current); const update = createUpdate(eventTime, lane); - update.payload = {isDehydrated: false}; - update.callback = - callback !== undefined && callback !== null ? callback : null; + // Caution: React DevTools currently depends on this property + // being called "element". + update.payload = {element: initialChildren}; enqueueUpdate(current, update, lane); scheduleInitialHydrationOnRoot(root, lane, eventTime); @@ -413,7 +409,7 @@ export function attemptSynchronousHydration(fiber: Fiber): void { switch (fiber.tag) { case HostRoot: const root: FiberRoot = fiber.stateNode; - if (isRootDehydrated(root)) { + if (root.isDehydrated) { // Flush the first scheduled "update". const lanes = getHighestPriorityPendingLanes(root); flushRoot(root, lanes); diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index f7166095ba13a..4970b685b1c1a 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -48,7 +48,6 @@ import { isContextProvider as isLegacyContextProvider, } from './ReactFiberContext.old'; import {createFiberRoot} from './ReactFiberRoot.old'; -import {isRootDehydrated} from './ReactFiberShellHydration'; import { injectInternals, markRenderScheduled, @@ -246,6 +245,9 @@ function findHostInstanceWithWarning( export function createContainer( containerInfo: Container, tag: RootTag, + // TODO: We can remove hydration-specific stuff from createContainer once + // we delete legacy mode. The new root API uses createHydrationContainer. + hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, @@ -253,13 +255,10 @@ export function createContainer( onRecoverableError: (error: mixed) => void, transitionCallbacks: null | TransitionTracingCallbacks, ): OpaqueRoot { - const hydrate = false; - const initialChildren = null; return createFiberRoot( containerInfo, tag, hydrate, - initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, @@ -271,8 +270,6 @@ export function createContainer( export function createHydrationContainer( initialChildren: ReactNodeList, - // TODO: Remove `callback` when we delete legacy mode. - callback: ?Function, containerInfo: Container, tag: RootTag, hydrationCallbacks: null | SuspenseHydrationCallbacks, @@ -287,7 +284,6 @@ export function createHydrationContainer( containerInfo, tag, hydrate, - initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, @@ -306,9 +302,9 @@ export function createHydrationContainer( const eventTime = requestEventTime(); const lane = requestUpdateLane(current); const update = createUpdate(eventTime, lane); - update.payload = {isDehydrated: false}; - update.callback = - callback !== undefined && callback !== null ? callback : null; + // Caution: React DevTools currently depends on this property + // being called "element". + update.payload = {element: initialChildren}; enqueueUpdate(current, update, lane); scheduleInitialHydrationOnRoot(root, lane, eventTime); @@ -413,7 +409,7 @@ export function attemptSynchronousHydration(fiber: Fiber): void { switch (fiber.tag) { case HostRoot: const root: FiberRoot = fiber.stateNode; - if (isRootDehydrated(root)) { + if (root.isDehydrated) { // Flush the first scheduled "update". const lanes = getHighestPriorityPendingLanes(root); flushRoot(root, lanes); diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index 7ff03ceead0e3..00dd694be4f5f 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -7,7 +7,6 @@ * @flow */ -import type {ReactNodeList} from 'shared/ReactTypes'; import type { FiberRoot, SuspenseHydrationCallbacks, @@ -40,8 +39,7 @@ import {createCache, retainCache} from './ReactFiberCacheComponent.new'; export type RootState = { element: any, - isDehydrated: boolean, - cache: Cache, + cache: Cache | null, transitions: Transitions | null, }; @@ -61,6 +59,7 @@ function FiberRootNode( this.timeoutHandle = noTimeout; this.context = null; this.pendingContext = null; + this.isDehydrated = hydrate; this.callbackNode = null; this.callbackPriority = NoLane; this.eventTimes = createLaneMap(NoLanes); @@ -129,7 +128,6 @@ export function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, - initialChildren: ReactNodeList, hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, @@ -180,17 +178,15 @@ export function createFiberRoot( root.pooledCache = initialCache; retainCache(initialCache); const initialState: RootState = { - element: initialChildren, - isDehydrated: hydrate, + element: null, cache: initialCache, transitions: null, }; uninitializedFiber.memoizedState = initialState; } else { const initialState: RootState = { - element: initialChildren, - isDehydrated: hydrate, - cache: (null: any), // not enabled yet + element: null, + cache: null, transitions: null, }; uninitializedFiber.memoizedState = initialState; diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index 179b9c17ae416..1e561e49facb3 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -7,7 +7,6 @@ * @flow */ -import type {ReactNodeList} from 'shared/ReactTypes'; import type { FiberRoot, SuspenseHydrationCallbacks, @@ -40,8 +39,7 @@ import {createCache, retainCache} from './ReactFiberCacheComponent.old'; export type RootState = { element: any, - isDehydrated: boolean, - cache: Cache, + cache: Cache | null, transitions: Transitions | null, }; @@ -61,6 +59,7 @@ function FiberRootNode( this.timeoutHandle = noTimeout; this.context = null; this.pendingContext = null; + this.isDehydrated = hydrate; this.callbackNode = null; this.callbackPriority = NoLane; this.eventTimes = createLaneMap(NoLanes); @@ -129,7 +128,6 @@ export function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, - initialChildren: ReactNodeList, hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, @@ -180,17 +178,15 @@ export function createFiberRoot( root.pooledCache = initialCache; retainCache(initialCache); const initialState: RootState = { - element: initialChildren, - isDehydrated: hydrate, + element: null, cache: initialCache, transitions: null, }; uninitializedFiber.memoizedState = initialState; } else { const initialState: RootState = { - element: initialChildren, - isDehydrated: hydrate, - cache: (null: any), // not enabled yet + element: null, + cache: null, transitions: null, }; uninitializedFiber.memoizedState = initialState; diff --git a/packages/react-reconciler/src/ReactFiberShellHydration.js b/packages/react-reconciler/src/ReactFiberShellHydration.js deleted file mode 100644 index caadb978f69d0..0000000000000 --- a/packages/react-reconciler/src/ReactFiberShellHydration.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {FiberRoot} from './ReactInternalTypes'; -import type {RootState} from './ReactFiberRoot.new'; - -// This is imported by the event replaying implementation in React DOM. It's -// in a separate file to break a circular dependency between the renderer and -// the reconciler. -export function isRootDehydrated(root: FiberRoot) { - const currentState: RootState = root.current.memoizedState; - return currentState.isDehydrated; -} diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 558440effa77a..7223ad7d052b0 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -88,7 +88,6 @@ import { createWorkInProgress, assignFiberPropertiesInDEV, } from './ReactFiber.new'; -import {isRootDehydrated} from './ReactFiberShellHydration'; import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode'; import { HostRoot, @@ -110,7 +109,6 @@ import { StoreConsistency, HostEffectMask, Hydrating, - ForceClientRender, BeforeMutationMask, MutationMask, LayoutMask, @@ -583,7 +581,34 @@ export function scheduleUpdateOnFiber( } } - if (root === workInProgressRoot) { + if (root.isDehydrated && root.tag !== LegacyRoot) { + // This root's shell hasn't hydrated yet. Revert to client rendering. + if (workInProgressRoot === root) { + // If this happened during an interleaved event, interrupt the + // in-progress hydration. Theoretically, we could attempt to force a + // synchronous hydration before switching to client rendering, but the + // most common reason the shell hasn't hydrated yet is because it + // suspended. So it's very likely to suspend again anyway. For + // simplicity, we'll skip that atttempt and go straight to + // client rendering. + // + // Another way to model this would be to give the initial hydration its + // own special lane. However, it may not be worth adding a lane solely + // for this purpose, so we'll wait until we find another use case before + // adding it. + // + // TODO: Consider only interrupting hydration if the priority of the + // update is higher than default. + prepareFreshStack(root, NoLanes); + } + root.isDehydrated = false; + const error = new Error( + 'This root received an early update, before anything was able ' + + 'hydrate. Switched the entire root to client rendering.', + ); + const onRecoverableError = root.onRecoverableError; + onRecoverableError(error); + } else if (root === workInProgressRoot) { // TODO: Consolidate with `isInterleavedUpdate` check // Received an update to a tree that's in the middle of rendering. Mark @@ -991,42 +1016,28 @@ function performConcurrentWorkOnRoot(root, didTimeout) { function recoverFromConcurrentError(root, errorRetryLanes) { // If an error occurred during hydration, discard server response and fall // back to client side render. - - // Before rendering again, save the errors from the previous attempt. - const errorsFromFirstAttempt = workInProgressRootConcurrentErrors; - - if (isRootDehydrated(root)) { - // The shell failed to hydrate. Set a flag to force a client rendering - // during the next attempt. To do this, we call prepareFreshStack now - // to create the root work-in-progress fiber. This is a bit weird in terms - // of factoring, because it relies on renderRootSync not calling - // prepareFreshStack again in the call below, which happens because the - // root and lanes haven't changed. - // - // TODO: I think what we should do is set ForceClientRender inside - // throwException, like we do for nested Suspense boundaries. The reason - // it's here instead is so we can switch to the synchronous work loop, too. - // Something to consider for a future refactor. - const rootWorkInProgress = prepareFreshStack(root, errorRetryLanes); - rootWorkInProgress.flags |= ForceClientRender; + if (root.isDehydrated) { + root.isDehydrated = false; if (__DEV__) { errorHydratingContainer(root.containerInfo); } + const error = new Error( + 'There was an error while hydrating. Because the error happened outside ' + + 'of a Suspense boundary, the entire root will switch to ' + + 'client rendering.', + ); + renderDidError(error); } + const errorsFromFirstAttempt = workInProgressRootConcurrentErrors; const exitStatus = renderRootSync(root, errorRetryLanes); if (exitStatus !== RootErrored) { // Successfully finished rendering on retry - - // The errors from the failed first attempt have been recovered. Add - // them to the collection of recoverable errors. We'll log them in the - // commit phase. - const errorsFromSecondAttempt = workInProgressRootRecoverableErrors; - workInProgressRootRecoverableErrors = errorsFromFirstAttempt; - // The errors from the second attempt should be queued after the errors - // from the first attempt, to preserve the causal sequence. - if (errorsFromSecondAttempt !== null) { - queueRecoverableErrors(errorsFromSecondAttempt); + if (errorsFromFirstAttempt !== null) { + // The errors from the failed first attempt have been recovered. Add + // them to the collection of recoverable errors. We'll log them in the + // commit phase. + queueRecoverableErrors(errorsFromFirstAttempt); } } else { // The UI failed to recover. @@ -1442,7 +1453,7 @@ export function popRenderLanes(fiber: Fiber) { popFromStack(subtreeRenderLanesCursor, fiber); } -function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { +function prepareFreshStack(root: FiberRoot, lanes: Lanes) { root.finishedWork = null; root.finishedLanes = NoLanes; @@ -1468,8 +1479,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { } } workInProgressRoot = root; - const rootWorkInProgress = createWorkInProgress(root.current, null); - workInProgress = rootWorkInProgress; + workInProgress = createWorkInProgress(root.current, null); workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes; workInProgressRootExitStatus = RootInProgress; workInProgressRootFatalError = null; @@ -1485,8 +1495,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { if (__DEV__) { ReactStrictModeWarnings.discardPendingWarnings(); } - - return rootWorkInProgress; } function handleError(root, thrownValue): void { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index c1c090d82b0b5..d8bb6b16e29fb 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -88,7 +88,6 @@ import { createWorkInProgress, assignFiberPropertiesInDEV, } from './ReactFiber.old'; -import {isRootDehydrated} from './ReactFiberShellHydration'; import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode'; import { HostRoot, @@ -110,7 +109,6 @@ import { StoreConsistency, HostEffectMask, Hydrating, - ForceClientRender, BeforeMutationMask, MutationMask, LayoutMask, @@ -583,7 +581,34 @@ export function scheduleUpdateOnFiber( } } - if (root === workInProgressRoot) { + if (root.isDehydrated && root.tag !== LegacyRoot) { + // This root's shell hasn't hydrated yet. Revert to client rendering. + if (workInProgressRoot === root) { + // If this happened during an interleaved event, interrupt the + // in-progress hydration. Theoretically, we could attempt to force a + // synchronous hydration before switching to client rendering, but the + // most common reason the shell hasn't hydrated yet is because it + // suspended. So it's very likely to suspend again anyway. For + // simplicity, we'll skip that atttempt and go straight to + // client rendering. + // + // Another way to model this would be to give the initial hydration its + // own special lane. However, it may not be worth adding a lane solely + // for this purpose, so we'll wait until we find another use case before + // adding it. + // + // TODO: Consider only interrupting hydration if the priority of the + // update is higher than default. + prepareFreshStack(root, NoLanes); + } + root.isDehydrated = false; + const error = new Error( + 'This root received an early update, before anything was able ' + + 'hydrate. Switched the entire root to client rendering.', + ); + const onRecoverableError = root.onRecoverableError; + onRecoverableError(error); + } else if (root === workInProgressRoot) { // TODO: Consolidate with `isInterleavedUpdate` check // Received an update to a tree that's in the middle of rendering. Mark @@ -991,42 +1016,28 @@ function performConcurrentWorkOnRoot(root, didTimeout) { function recoverFromConcurrentError(root, errorRetryLanes) { // If an error occurred during hydration, discard server response and fall // back to client side render. - - // Before rendering again, save the errors from the previous attempt. - const errorsFromFirstAttempt = workInProgressRootConcurrentErrors; - - if (isRootDehydrated(root)) { - // The shell failed to hydrate. Set a flag to force a client rendering - // during the next attempt. To do this, we call prepareFreshStack now - // to create the root work-in-progress fiber. This is a bit weird in terms - // of factoring, because it relies on renderRootSync not calling - // prepareFreshStack again in the call below, which happens because the - // root and lanes haven't changed. - // - // TODO: I think what we should do is set ForceClientRender inside - // throwException, like we do for nested Suspense boundaries. The reason - // it's here instead is so we can switch to the synchronous work loop, too. - // Something to consider for a future refactor. - const rootWorkInProgress = prepareFreshStack(root, errorRetryLanes); - rootWorkInProgress.flags |= ForceClientRender; + if (root.isDehydrated) { + root.isDehydrated = false; if (__DEV__) { errorHydratingContainer(root.containerInfo); } + const error = new Error( + 'There was an error while hydrating. Because the error happened outside ' + + 'of a Suspense boundary, the entire root will switch to ' + + 'client rendering.', + ); + renderDidError(error); } + const errorsFromFirstAttempt = workInProgressRootConcurrentErrors; const exitStatus = renderRootSync(root, errorRetryLanes); if (exitStatus !== RootErrored) { // Successfully finished rendering on retry - - // The errors from the failed first attempt have been recovered. Add - // them to the collection of recoverable errors. We'll log them in the - // commit phase. - const errorsFromSecondAttempt = workInProgressRootRecoverableErrors; - workInProgressRootRecoverableErrors = errorsFromFirstAttempt; - // The errors from the second attempt should be queued after the errors - // from the first attempt, to preserve the causal sequence. - if (errorsFromSecondAttempt !== null) { - queueRecoverableErrors(errorsFromSecondAttempt); + if (errorsFromFirstAttempt !== null) { + // The errors from the failed first attempt have been recovered. Add + // them to the collection of recoverable errors. We'll log them in the + // commit phase. + queueRecoverableErrors(errorsFromFirstAttempt); } } else { // The UI failed to recover. @@ -1442,7 +1453,7 @@ export function popRenderLanes(fiber: Fiber) { popFromStack(subtreeRenderLanesCursor, fiber); } -function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { +function prepareFreshStack(root: FiberRoot, lanes: Lanes) { root.finishedWork = null; root.finishedLanes = NoLanes; @@ -1468,8 +1479,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { } } workInProgressRoot = root; - const rootWorkInProgress = createWorkInProgress(root.current, null); - workInProgress = rootWorkInProgress; + workInProgress = createWorkInProgress(root.current, null); workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes; workInProgressRootExitStatus = RootInProgress; workInProgressRootFatalError = null; @@ -1485,8 +1495,6 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { if (__DEV__) { ReactStrictModeWarnings.discardPendingWarnings(); } - - return rootWorkInProgress; } function handleError(root, thrownValue): void { diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 1fa3d4b6680d1..dd2e09c03b210 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -213,6 +213,8 @@ type BaseFiberRootProperties = {| // Top context object, used by renderSubtreeIntoContainer context: Object | null, pendingContext: Object | null, + // Determines if we should attempt to hydrate on the initial mount + +isDehydrated: boolean, // Used by useMutableSource hook to avoid tearing during hydration. mutableSourceEagerHydrationData?: Array< diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index 82e23de9965da..d0c3d5b236ea4 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js @@ -72,6 +72,7 @@ describe('ReactFiberHostContext', () => { const container = Renderer.createContainer( /* root: */ null, ConcurrentRoot, + false, null, false, '', @@ -135,6 +136,7 @@ describe('ReactFiberHostContext', () => { const container = Renderer.createContainer( rootContext, ConcurrentRoot, + false, null, false, '', diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 76911d701de79..e850086439a67 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -473,6 +473,7 @@ function create(element: React$Element, options: TestRendererOptions) { let root: FiberRoot | null = createContainer( container, isConcurrent ? ConcurrentRoot : LegacyRoot, + false, null, isStrictMode, concurrentUpdatesByDefault,