Skip to content

Commit

Permalink
Bugfix: Remove extra render pass when reverting to client render (#26445
Browse files Browse the repository at this point in the history
)

(This was reviewed and approved as part of #26380; I'm extracting it
into its own PR so that it can bisected later if it causes an issue.)

I noticed while working on a PR that when an error happens during
hydration, and we revert to client rendering, React actually does _two_
additional render passes instead of just one. We didn't notice it
earlier because none of our tests happened to assert on how many renders
it took to recover, only on the final output.

It's possible this extra render pass had other consequences that I'm not
aware of, like messing with some assumption in the recoverable errors
logic.

This adds a test to demonstrate the issue. (One problem is that we don't
have much test coverage of this scenario in the first place, which
likely would have caught this earlier.)
  • Loading branch information
acdlite authored Mar 21, 2023
1 parent 520f7f3 commit 77ba161
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ let ReactFeatureFlags;
let Suspense;
let SuspenseList;
let Offscreen;
let useSyncExternalStore;
let act;
let IdleEventPriority;
let waitForAll;
Expand Down Expand Up @@ -113,6 +114,7 @@ describe('ReactDOMServerPartialHydration', () => {
Scheduler = require('scheduler');
Suspense = React.Suspense;
Offscreen = React.unstable_Offscreen;
useSyncExternalStore = React.useSyncExternalStore;
if (gate(flags => flags.enableSuspenseList)) {
SuspenseList = React.SuspenseList;
}
Expand Down Expand Up @@ -480,20 +482,42 @@ describe('ReactDOMServerPartialHydration', () => {
});

it('recovers with client render when server rendered additional nodes at suspense root', async () => {
function CheckIfHydrating({children}) {
// This is a trick to check whether we're hydrating or not, since React
// doesn't expose that information currently except
// via useSyncExternalStore.
let serverOrClient = '(unknown)';
useSyncExternalStore(
() => {},
() => {
serverOrClient = 'Client rendered';
return null;
},
() => {
serverOrClient = 'Server rendered';
return null;
},
);
Scheduler.log(serverOrClient);
return null;
}

const ref = React.createRef();
function App({hasB}) {
return (
<div>
<Suspense fallback="Loading...">
<span ref={ref}>A</span>
{hasB ? <span>B</span> : null}
<CheckIfHydrating />
</Suspense>
<div>Sibling</div>
</div>
);
}

const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
assertLog(['Server rendered']);

const container = document.createElement('div');
container.innerHTML = finalHTML;
Expand All @@ -514,12 +538,12 @@ describe('ReactDOMServerPartialHydration', () => {
});
}).toErrorDev('Did not expect server HTML to contain a <span> in <div>');

jest.runAllTimers();

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');

assertLog([
'Server rendered',
'Client rendered',
'There was an error while hydrating this Suspense boundary. ' +
'Switched to client rendering.',
]);
Expand Down
6 changes: 2 additions & 4 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ import {
StaticMask,
MutationMask,
Passive,
Incomplete,
ShouldCapture,
ForceClientRender,
SuspenseyCommit,
ScheduleRetry,
Expand Down Expand Up @@ -839,7 +837,7 @@ function completeDehydratedSuspenseBoundary(
) {
warnIfUnhydratedTailNodes(workInProgress);
resetHydrationState();
workInProgress.flags |= ForceClientRender | Incomplete | ShouldCapture;
workInProgress.flags |= ForceClientRender | DidCapture;

return false;
}
Expand Down Expand Up @@ -1284,7 +1282,7 @@ function completeWork(
nextState,
);
if (!fallthroughToNormalSuspensePath) {
if (workInProgress.flags & ShouldCapture) {
if (workInProgress.flags & ForceClientRender) {
// Special case. There were remaining unhydrated nodes. We treat
// this as a mismatch. Revert to client rendering.
return workInProgress;
Expand Down

0 comments on commit 77ba161

Please sign in to comment.