[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);