Skip to content

Commit c396018

Browse files
committed
Revert to client render on text content mismatch
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.
1 parent 5d70f7a commit c396018

File tree

3 files changed

+76
-3
lines changed

3 files changed

+76
-3
lines changed

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

+71
Original file line numberDiff line numberDiff line change
@@ -3361,4 +3361,75 @@ describe('ReactDOMServerPartialHydration', () => {
33613361
'<div>1</div><span>client</span><div>2</div>',
33623362
);
33633363
});
3364+
3365+
it("falls back to client rendering when there's a text mismatch (direct text child)", async () => {
3366+
function DirectTextChild({text}) {
3367+
return <div>{text}</div>;
3368+
}
3369+
const container = document.createElement('div');
3370+
container.innerHTML = ReactDOMServer.renderToString(
3371+
<DirectTextChild text="good" />,
3372+
);
3373+
expect(() => {
3374+
act(() => {
3375+
ReactDOM.hydrateRoot(container, <DirectTextChild text="bad" />, {
3376+
onRecoverableError(error) {
3377+
Scheduler.unstable_yieldValue(error.message);
3378+
},
3379+
});
3380+
});
3381+
}).toErrorDev(
3382+
[
3383+
'Text content did not match. Server: "good" Client: "bad"',
3384+
'An error occurred during hydration. The server HTML was replaced with ' +
3385+
'client content in <div>.',
3386+
],
3387+
{withoutStack: 1},
3388+
);
3389+
expect(Scheduler).toHaveYielded([
3390+
'Text content does not match server-rendered HTML.',
3391+
'There was an error while hydrating. Because the error happened outside ' +
3392+
'of a Suspense boundary, the entire root will switch to client rendering.',
3393+
]);
3394+
});
3395+
3396+
it("falls back to client rendering when there's a text mismatch (text child with siblings)", async () => {
3397+
function Sibling() {
3398+
return 'Sibling';
3399+
}
3400+
3401+
function TextChildWithSibling({text}) {
3402+
return (
3403+
<div>
3404+
<Sibling />
3405+
{text}
3406+
</div>
3407+
);
3408+
}
3409+
const container2 = document.createElement('div');
3410+
container2.innerHTML = ReactDOMServer.renderToString(
3411+
<TextChildWithSibling text="good" />,
3412+
);
3413+
expect(() => {
3414+
act(() => {
3415+
ReactDOM.hydrateRoot(container2, <TextChildWithSibling text="bad" />, {
3416+
onRecoverableError(error) {
3417+
Scheduler.unstable_yieldValue(error.message);
3418+
},
3419+
});
3420+
});
3421+
}).toErrorDev(
3422+
[
3423+
'Text content did not match. Server: "good" Client: "bad"',
3424+
'An error occurred during hydration. The server HTML was replaced with ' +
3425+
'client content in <div>.',
3426+
],
3427+
{withoutStack: 1},
3428+
);
3429+
expect(Scheduler).toHaveYielded([
3430+
'Text content does not match server-rendered HTML.',
3431+
'There was an error while hydrating. Because the error happened outside ' +
3432+
'of a Suspense boundary, the entire root will switch to client rendering.',
3433+
]);
3434+
});
33643435
});

packages/react-dom/src/client/ReactDOMComponent.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,9 @@ export function checkForUnmatchedText(
249249
}
250250

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

scripts/error-codes/codes.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -408,5 +408,6 @@
408408
"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.",
409409
"421": "There was an error while hydrating this Suspense boundary. Switched to client rendering.",
410410
"422": "There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.",
411-
"423": "This root received an early update, before anything was able hydrate. Switched the entire root to client rendering."
411+
"423": "This root received an early update, before anything was able hydrate. Switched the entire root to client rendering.",
412+
"424": "Text content does not match server-rendered HTML."
412413
}

0 commit comments

Comments
 (0)