Skip to content

Commit 98001e4

Browse files
committed
Don't hydrate Elements that have dangerouslySetInnerHTML and suppressHydrationWarning
1 parent ab18f33 commit 98001e4

3 files changed

Lines changed: 63 additions & 4 deletions

File tree

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2618,6 +2618,10 @@ function diffHydratedGenericElement(
26182618
// Noop
26192619
continue;
26202620
case 'dangerouslySetInnerHTML':
2621+
// Skip innerHTML comparison only when suppressHydrationWarning is also set
2622+
if (props.suppressHydrationWarning === true) {
2623+
continue;
2624+
}
26212625
const serverHTML = domElement.innerHTML;
26222626
const nextHtml = value ? value.__html : undefined;
26232627
if (nextHtml != null) {
@@ -3220,10 +3224,12 @@ export function hydrateProperties(
32203224
// even listeners these nodes might be wired up to.
32213225
// TODO: Warn if there is more than a single textNode as a child.
32223226
// TODO: Should we use domElement.firstChild.nodeValue to compare?
3227+
// Skip text content check if both dangerouslySetInnerHTML and suppressHydrationWarning are present
32233228
if (
3224-
typeof children === 'string' ||
3225-
typeof children === 'number' ||
3226-
typeof children === 'bigint'
3229+
!(props.dangerouslySetInnerHTML != null && props.suppressHydrationWarning === true) &&
3230+
(typeof children === 'string' ||
3231+
typeof children === 'number' ||
3232+
typeof children === 'bigint')
32273233
) {
32283234
if (
32293235
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint

packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,4 +802,46 @@ describe('ReactDOMServerHydration', () => {
802802

803803
expect(ref.current).toBe(button);
804804
});
805+
806+
it('should skip hydration of elements with dangerouslySetInnerHTML and suppressHydrationWarning', async () => {
807+
const htmlContent = {
808+
__html: '<p>This is <strong>HTML</strong> content</p>',
809+
};
810+
811+
function TestComponent() {
812+
return (
813+
<div>
814+
<h1>Header</h1>
815+
<div
816+
className="testElement"
817+
dangerouslySetInnerHTML={htmlContent}
818+
suppressHydrationWarning={true}
819+
/>
820+
<p>Footer</p>
821+
</div>
822+
);
823+
}
824+
825+
const container = document.createElement('div');
826+
container.innerHTML = ReactDOMServer.renderToString(<TestComponent />);
827+
828+
const testElement = container.querySelector('.testElement');
829+
expect(testElement).not.toBe(null);
830+
expect(testElement.innerHTML).toBe(
831+
'<p>This is <strong>HTML</strong> content</p>',
832+
);
833+
834+
// change content before hydration to simulate a mismatch
835+
testElement.innerHTML = '<h1>Content changed</h1>';
836+
837+
// Hydrate - should not produce warnings when both props are set
838+
await act(() => {
839+
ReactDOMClient.hydrateRoot(container, <TestComponent />);
840+
});
841+
842+
// Verify the innerHTML is preserved (not cleared or modified)
843+
expect(testElement.innerHTML).toBe(
844+
'<h1>Content changed</h1>',
845+
);
846+
});
805847
});

packages/react-reconciler/src/ReactFiberHydrationContext.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,18 @@ function tryHydrateInstance(
276276
}
277277

278278
hydrationParentFiber = fiber;
279-
nextHydratableInstance = getFirstHydratableChild(instance);
279+
// Skip hydrating children if this element has both suppressHydrationWarning
280+
// and dangerouslySetInnerHTML - this allows developers to opt-out of
281+
// hydration validation for elements where server/client HTML intentionally differs
282+
const props = fiber.pendingProps;
283+
if (
284+
props.dangerouslySetInnerHTML != null &&
285+
props.suppressHydrationWarning === true
286+
) {
287+
nextHydratableInstance = null;
288+
} else {
289+
nextHydratableInstance = getFirstHydratableChild(instance);
290+
}
280291
rootOrSingletonContext = false;
281292
return true;
282293
}

0 commit comments

Comments
 (0)