diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
index f4df63ca7a652..999fbd9f7190d 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzShellHydration-test.js
@@ -548,4 +548,79 @@ describe('ReactDOMFizzShellHydration', () => {
]);
expect(container.textContent).toBe('Hello world');
});
+
+ it(
+ 'handles suspending while recovering from a hydration error (in the ' +
+ 'shell, no Suspense boundary)',
+ async () => {
+ const useSyncExternalStore = React.useSyncExternalStore;
+
+ let isClient = false;
+
+ let resolve;
+ const clientPromise = new Promise(res => {
+ resolve = res;
+ });
+
+ function App() {
+ const state = useSyncExternalStore(
+ function subscribe() {
+ return () => {};
+ },
+ function getSnapshot() {
+ return 'Client';
+ },
+ function getServerSnapshot() {
+ const isHydrating = isClient;
+ if (isHydrating) {
+ // This triggers an error during hydration
+ throw new Error('Oops!');
+ }
+ return 'Server';
+ },
+ );
+
+ if (state === 'Client') {
+ return React.use(clientPromise);
+ }
+
+ return state;
+ }
+
+ // Server render
+ await serverAct(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+ assertLog([]);
+
+ expect(container.innerHTML).toBe('Server');
+
+ // During hydration, an error is thrown. React attempts to recover by
+ // switching to client render
+ isClient = true;
+ await clientAct(async () => {
+ ReactDOMClient.hydrateRoot(container, , {
+ onRecoverableError(error) {
+ Scheduler.log('onRecoverableError: ' + error.message);
+ if (error.cause) {
+ Scheduler.log('Cause: ' + error.cause.message);
+ }
+ },
+ });
+ });
+ expect(container.innerHTML).toBe('Server'); // Still suspended
+ assertLog([]);
+
+ await clientAct(async () => {
+ resolve('Client');
+ });
+ assertLog([
+ 'onRecoverableError: There was an error while hydrating but React was ' +
+ 'able to recover by instead client rendering the entire root.',
+ 'Cause: Oops!',
+ ]);
+ expect(container.innerHTML).toBe('Client');
+ },
+ );
});
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index 17c1760755a7f..8b0c8729506cc 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -931,19 +931,32 @@ export function performConcurrentWorkOnRoot(
// Check if something threw
if (exitStatus === RootErrored) {
- const originallyAttemptedLanes = lanes;
+ const lanesThatJustErrored = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
- originallyAttemptedLanes,
+ lanesThatJustErrored,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(
root,
- originallyAttemptedLanes,
+ lanesThatJustErrored,
errorRetryLanes,
);
renderWasConcurrent = false;
+ // Need to check the exit status again.
+ if (exitStatus !== RootErrored) {
+ // The root did not error this time. Restart the exit algorithm
+ // from the beginning.
+ // TODO: Refactor the exit algorithm to be less confusing. Maybe
+ // more branches + recursion instead of a loop. I think the only
+ // thing that causes it to be a loop is the RootDidNotComplete
+ // check. If that's true, then we don't need a loop/recursion
+ // at all.
+ continue;
+ } else {
+ // The root errored yet again. Proceed to commit the tree.
+ }
}
}
if (exitStatus === RootFatalErrored) {