@samitouri / QOS-React-2 / commits / b8f50c9394

[Fiber] Don't set .innerHTML when it hasn't changed (#36949)

Fixes #30994. We compared this previously but it regressed in #26501.

Sophie Alpert committed Jul 7, 2026 at 09:09 UTC b8f50c93942929e0736941d2065bce5598d6484b
2 files changed +45 -2
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+10 -2
@@ -653,7 +653,11 @@ function setProp(
653 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
654 );
655 }
656 - domElement.innerHTML = nextHtml;
656 + const lastHtml: any =
657 + prevValue != null ? (prevValue as any).__html : undefined;
658 + if (lastHtml !== nextHtml) {
659 + domElement.innerHTML = nextHtml;
660 + }
661 }
662 }
663 break;
@@ -1014,7 +1018,11 @@ function setPropOnCustomElement(
1018 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
1019 );
1020 }
1017 - domElement.innerHTML = nextHtml;
1021 + const lastHtml: any =
1022 + prevValue != null ? (prevValue as any).__html : undefined;
1023 + if (lastHtml !== nextHtml) {
1024 + domElement.innerHTML = nextHtml;
1025 + }
1026 }
1027 }
1028 break;
packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+35
@@ -1283,6 +1283,41 @@ describe('ReactDOMComponent', () => {
1283 expect(container.textContent).toEqual('bonjour');
1284 });
1285
1286 + it('should not incur unnecessary DOM mutations for equal innerHTML', async () => {
1287 + // Regression test for https://github.com/facebook/react/issues/30994.
1288 + // Reassigning equal innerHTML destroys and recreates the child nodes,
1289 + // which breaks in-progress gestures (e.g. swallows an in-flight click
1290 + // when a re-render commits between focus and click) and discards state
1291 + // like text selection.
1292 + const container = document.createElement('div');
1293 + const root = ReactDOMClient.createRoot(container);
1294 + await act(() => {
1295 + root.render(
1296 + <div dangerouslySetInnerHTML={{__html: '<span>hi</span>'}} />,
1297 + );
1298 + });
1299 +
1300 + const node = container.firstChild;
1301 + const child = node.firstChild;
1302 +
1303 + // A new object with an equal __html string must not touch the DOM.
1304 + await act(() => {
1305 + root.render(
1306 + <div dangerouslySetInnerHTML={{__html: '<span>hi</span>'}} />,
1307 + );
1308 + });
1309 + expect(node.firstChild).toBe(child);
1310 +
1311 + // A different __html string still updates.
1312 + await act(() => {
1313 + root.render(
1314 + <div dangerouslySetInnerHTML={{__html: '<span>bye</span>'}} />,
1315 + );
1316 + });
1317 + expect(node.firstChild).not.toBe(child);
1318 + expect(node.innerHTML).toEqual('<span>bye</span>');
1319 + });
1320 +
1321 it('should not incur unnecessary DOM mutations for attributes', async () => {
1322 const container = document.createElement('div');
1323 const root = ReactDOMClient.createRoot(container);