Skip to content

Commit

Permalink
Revert to client render on text content mismatch
Browse files Browse the repository at this point in the history
Expands the behavior of enableClientRenderFallbackOnHydrationMismatch to
check text content, too.

If the text is different from what was rendered on the server, we will
recover the UI by falling back to client rendering, up to the nearest
Suspense boundary.
  • Loading branch information
acdlite committed Feb 24, 2022
1 parent 5d70f7a commit c396018
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3361,4 +3361,75 @@ describe('ReactDOMServerPartialHydration', () => {
'<div>1</div><span>client</span><div>2</div>',
);
});

it("falls back to client rendering when there's a text mismatch (direct text child)", async () => {
function DirectTextChild({text}) {
return <div>{text}</div>;
}
const container = document.createElement('div');
container.innerHTML = ReactDOMServer.renderToString(
<DirectTextChild text="good" />,
);
expect(() => {
act(() => {
ReactDOM.hydrateRoot(container, <DirectTextChild text="bad" />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
});
}).toErrorDev(
[
'Text content did not match. Server: "good" Client: "bad"',
'An error occurred during hydration. The server HTML was replaced with ' +
'client content in <div>.',
],
{withoutStack: 1},
);
expect(Scheduler).toHaveYielded([
'Text content does not match server-rendered HTML.',
'There was an error while hydrating. Because the error happened outside ' +
'of a Suspense boundary, the entire root will switch to client rendering.',
]);
});

it("falls back to client rendering when there's a text mismatch (text child with siblings)", async () => {
function Sibling() {
return 'Sibling';
}

function TextChildWithSibling({text}) {
return (
<div>
<Sibling />
{text}
</div>
);
}
const container2 = document.createElement('div');
container2.innerHTML = ReactDOMServer.renderToString(
<TextChildWithSibling text="good" />,
);
expect(() => {
act(() => {
ReactDOM.hydrateRoot(container2, <TextChildWithSibling text="bad" />, {
onRecoverableError(error) {
Scheduler.unstable_yieldValue(error.message);
},
});
});
}).toErrorDev(
[
'Text content did not match. Server: "good" Client: "bad"',
'An error occurred during hydration. The server HTML was replaced with ' +
'client content in <div>.',
],
{withoutStack: 1},
);
expect(Scheduler).toHaveYielded([
'Text content does not match server-rendered HTML.',
'There was an error while hydrating. Because the error happened outside ' +
'of a Suspense boundary, the entire root will switch to client rendering.',
]);
});
});
5 changes: 3 additions & 2 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,9 @@ export function checkForUnmatchedText(
}

if (isConcurrentMode && enableClientRenderFallbackOnHydrationMismatch) {
// TODO: In concurrent roots, we will throw when there's a text mismatch
// and revert to client rendering, up to the nearest Suspense boundary.
// In concurrent roots, we throw when there's a text mismatch and revert to
// client rendering, up to the nearest Suspense boundary.
throw new Error('Text content does not match server-rendered HTML.');
}
}

Expand Down
3 changes: 2 additions & 1 deletion scripts/error-codes/codes.json
Original file line number Diff line number Diff line change
Expand Up @@ -408,5 +408,6 @@
"420": "This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.",
"421": "There was an error while hydrating this Suspense boundary. Switched to client rendering.",
"422": "There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.",
"423": "This root received an early update, before anything was able hydrate. Switched the entire root to client rendering."
"423": "This root received an early update, before anything was able hydrate. Switched the entire root to client rendering.",
"424": "Text content does not match server-rendered HTML."
}

0 comments on commit c396018

Please sign in to comment.