diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 0782b57ff4b47..1ecab933035f8 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -1864,6 +1864,83 @@ describe('ReactDOMFizzServer', () => { } }); + // @gate experimental + it( + 'errors during hydration in the shell force a client render at the ' + + 'root, and during the client render it recovers', + async () => { + let isClient = false; + + function subscribe() { + return () => {}; + } + function getClientSnapshot() { + return 'Yay!'; + } + + // At the time of writing, the only API that exposes whether it's currently + // hydrating is the `getServerSnapshot` API, so I'm using that here to + // simulate an error during hydration. + function getServerSnapshot() { + if (isClient) { + throw new Error('Hydration error'); + } + return 'Yay!'; + } + + function Child() { + const value = useSyncExternalStore( + subscribe, + getClientSnapshot, + getServerSnapshot, + ); + Scheduler.unstable_yieldValue(value); + return value; + } + + const spanRef = React.createRef(); + + function App() { + return ( + + + + ); + } + + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + expect(Scheduler).toHaveYielded(['Yay!']); + + const span = container.getElementsByTagName('span')[0]; + + // Hydrate the tree. Child will throw during hydration, but not when it + // falls back to client rendering. + isClient = true; + ReactDOM.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + + // An error logged but instead of surfacing it to the UI, we switched + // to client rendering. + expect(() => { + expect(Scheduler).toFlushAndYield(['Yay!', 'Hydration error']); + }).toErrorDev( + 'An error occurred during hydration. The server HTML was replaced', + {withoutStack: true}, + ); + expect(getVisibleChildren(container)).toEqual(Yay!); + + // The node that's inside the boundary that errored during hydration was + // not hydrated. + expect(spanRef.current).not.toBe(span); + }, + ); + // @gate experimental it( 'errors during hydration force a client render at the nearest Suspense ' + diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 521a75d11dcb7..f0d7b6c625cc6 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -225,9 +225,6 @@ import { markSkippedUpdateLanes, getWorkInProgressRoot, pushRenderLanes, - getExecutionContext, - RetryAfterError, - NoContext, } from './ReactFiberWorkLoop.new'; import {setWorkInProgressVersion} from './ReactMutableSource.new'; import { @@ -2646,14 +2643,6 @@ function updateDehydratedSuspenseComponent( // but after we've already committed once. warnIfHydrating(); - if ((getExecutionContext() & RetryAfterError) !== NoContext) { - return retrySuspenseComponentWithoutHydrating( - current, - workInProgress, - renderLanes, - ); - } - if ((workInProgress.mode & ConcurrentMode) === NoMode) { return retrySuspenseComponentWithoutHydrating( current, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 471bb11d941da..b562291850ea5 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -225,9 +225,6 @@ import { markSkippedUpdateLanes, getWorkInProgressRoot, pushRenderLanes, - getExecutionContext, - RetryAfterError, - NoContext, } from './ReactFiberWorkLoop.old'; import {setWorkInProgressVersion} from './ReactMutableSource.old'; import { @@ -2646,14 +2643,6 @@ function updateDehydratedSuspenseComponent( // but after we've already committed once. warnIfHydrating(); - if ((getExecutionContext() & RetryAfterError) !== NoContext) { - return retrySuspenseComponentWithoutHydrating( - current, - workInProgress, - renderLanes, - ); - } - if ((workInProgress.mode & ConcurrentMode) === NoMode) { return retrySuspenseComponentWithoutHydrating( current, diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 4a1880e0cf848..12bc721c6d399 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -245,11 +245,10 @@ const { type ExecutionContext = number; -export const NoContext = /* */ 0b0000; -const BatchedContext = /* */ 0b0001; -const RenderContext = /* */ 0b0010; -const CommitContext = /* */ 0b0100; -export const RetryAfterError = /* */ 0b1000; +export const NoContext = /* */ 0b000; +const BatchedContext = /* */ 0b001; +const RenderContext = /* */ 0b010; +const CommitContext = /* */ 0b100; type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6; const RootInProgress = 0; @@ -945,9 +944,6 @@ function performConcurrentWorkOnRoot(root, didTimeout) { } function recoverFromConcurrentError(root, errorRetryLanes) { - const prevExecutionContext = executionContext; - executionContext |= RetryAfterError; - // If an error occurred during hydration, discard server response and fall // back to client side render. if (root.isDehydrated) { @@ -970,9 +966,6 @@ function recoverFromConcurrentError(root, errorRetryLanes) { } else { // The UI failed to recover. } - - executionContext = prevExecutionContext; - return exitStatus; } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index b9fada5bd00b0..9e37f41f4fc82 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -245,11 +245,10 @@ const { type ExecutionContext = number; -export const NoContext = /* */ 0b0000; -const BatchedContext = /* */ 0b0001; -const RenderContext = /* */ 0b0010; -const CommitContext = /* */ 0b0100; -export const RetryAfterError = /* */ 0b1000; +export const NoContext = /* */ 0b000; +const BatchedContext = /* */ 0b001; +const RenderContext = /* */ 0b010; +const CommitContext = /* */ 0b100; type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6; const RootInProgress = 0; @@ -945,9 +944,6 @@ function performConcurrentWorkOnRoot(root, didTimeout) { } function recoverFromConcurrentError(root, errorRetryLanes) { - const prevExecutionContext = executionContext; - executionContext |= RetryAfterError; - // If an error occurred during hydration, discard server response and fall // back to client side render. if (root.isDehydrated) { @@ -970,9 +966,6 @@ function recoverFromConcurrentError(root, errorRetryLanes) { } else { // The UI failed to recover. } - - executionContext = prevExecutionContext; - return exitStatus; }