Skip to content
Open
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
12 changes: 9 additions & 3 deletions packages/react-dom-bindings/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2618,6 +2618,10 @@ function diffHydratedGenericElement(
// Noop
continue;
case 'dangerouslySetInnerHTML':
// Skip innerHTML comparison only when suppressHydrationWarning is also set
if (props.suppressHydrationWarning === true) {
continue;
}
const serverHTML = domElement.innerHTML;
const nextHtml = value ? value.__html : undefined;
if (nextHtml != null) {
Expand Down Expand Up @@ -3220,10 +3224,12 @@ export function hydrateProperties(
// even listeners these nodes might be wired up to.
// TODO: Warn if there is more than a single textNode as a child.
// TODO: Should we use domElement.firstChild.nodeValue to compare?
// Skip text content check if both dangerouslySetInnerHTML and suppressHydrationWarning are present
if (
typeof children === 'string' ||
typeof children === 'number' ||
typeof children === 'bigint'
!(props.dangerouslySetInnerHTML != null && props.suppressHydrationWarning === true) &&
(typeof children === 'string' ||
typeof children === 'number' ||
typeof children === 'bigint')
) {
if (
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -802,4 +802,46 @@ describe('ReactDOMServerHydration', () => {

expect(ref.current).toBe(button);
});

it('should skip hydration of elements with dangerouslySetInnerHTML and suppressHydrationWarning', async () => {
const htmlContent = {
__html: '<p>This is <strong>HTML</strong> content</p>',
};

function TestComponent() {
return (
<div>
<h1>Header</h1>
<div
className="testElement"
dangerouslySetInnerHTML={htmlContent}
suppressHydrationWarning={true}
/>
<p>Footer</p>
</div>
);
}

const container = document.createElement('div');
container.innerHTML = ReactDOMServer.renderToString(<TestComponent />);

const testElement = container.querySelector('.testElement');
expect(testElement).not.toBe(null);
expect(testElement.innerHTML).toBe(
'<p>This is <strong>HTML</strong> content</p>',
);

// change content before hydration to simulate a mismatch
testElement.innerHTML = '<h1>Content changed</h1>';

// Hydrate - should not produce warnings when both props are set
await act(() => {
ReactDOMClient.hydrateRoot(container, <TestComponent />);
});

// Verify the innerHTML is preserved (not cleared or modified)
expect(testElement.innerHTML).toBe(
'<h1>Content changed</h1>',
);
});
});
13 changes: 12 additions & 1 deletion packages/react-reconciler/src/ReactFiberHydrationContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,18 @@ function tryHydrateInstance(
}

hydrationParentFiber = fiber;
nextHydratableInstance = getFirstHydratableChild(instance);
// Skip hydrating children if this element has both suppressHydrationWarning
// and dangerouslySetInnerHTML - this allows developers to opt-out of
// hydration validation for elements where server/client HTML intentionally differs
const props = fiber.pendingProps;
if (
props.dangerouslySetInnerHTML != null &&
props.suppressHydrationWarning === true
) {
nextHydratableInstance = null;
} else {
nextHydratableInstance = getFirstHydratableChild(instance);
}
rootOrSingletonContext = false;
return true;
}
Expand Down