Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Suspend while recovering from hydration error #28800

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App />);
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, <App />, {
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');
},
);
});
19 changes: 16 additions & 3 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down