diff --git a/packages/react-dom-bindings/src/client/ReactDOMInput.js b/packages/react-dom-bindings/src/client/ReactDOMInput.js index b6e665e12883..7bc454a8fb71 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMInput.js +++ b/packages/react-dom-bindings/src/client/ReactDOMInput.js @@ -98,23 +98,50 @@ export function updateInput( ) { const node: HTMLInputElement = (element: any); - // Temporarily disconnect the input from any radio buttons. - // Changing the type or name as the same time as changing the checked value - // needs to be atomically applied. We can only ensure that by disconnecting - // the name while do the mutations and then reapply the name after that's done. - node.name = ''; - - if ( + const isTypeValid = type != null && typeof type !== 'function' && typeof type !== 'symbol' && - typeof type !== 'boolean' - ) { + typeof type !== 'boolean'; + const isNameValid = + name != null && + typeof name !== 'function' && + typeof name !== 'symbol' && + typeof name !== 'boolean'; + + // Determine if type or name is actually changing compared to the DOM. + // node.type is always lowercased by the browser per the HTML spec, so we + // lowercase the prop value to avoid false positives from casing differences. + let typeChanged; + if (isTypeValid) { if (__DEV__) { checkAttributeStringCoercion(type, 'type'); } - node.type = type; + typeChanged = node.type !== ('' + type).toLowerCase(); } else { + typeChanged = node.hasAttribute('type'); + } + const nameStr = isNameValid ? toString(getToStringValue(name)) : null; + const nameChanged = + nameStr !== null ? node.name !== nameStr : node.hasAttribute('name'); + + // Temporarily disconnect the input from any radio buttons. + // Changing the type or name at the same time as changing the checked value + // needs to be atomically applied. We can only ensure that by disconnecting + // the name while doing the mutations and then reapply the name after that's done. + // We only need to do this if type or name is actually changing. + if (typeChanged || nameChanged) { + node.name = ''; + } + + if (isTypeValid) { + if (__DEV__) { + checkAttributeStringCoercion(type, 'type'); + } + if (typeChanged) { + node.type = type; + } + } else if (typeChanged) { node.removeAttribute('type'); } @@ -188,17 +215,14 @@ export function updateInput( checked && typeof checked !== 'function' && typeof checked !== 'symbol'; } - if ( - name != null && - typeof name !== 'function' && - typeof name !== 'symbol' && - typeof name !== 'boolean' - ) { + if (isNameValid) { if (__DEV__) { checkAttributeStringCoercion(name, 'name'); } - node.name = toString(getToStringValue(name)); - } else { + if (typeChanged || nameChanged) { + node.name = (nameStr: any); + } + } else if (typeChanged || nameChanged) { node.removeAttribute('name'); } } diff --git a/packages/react-dom/src/__tests__/ReactDOMInput-test.js b/packages/react-dom/src/__tests__/ReactDOMInput-test.js index 04bd96fe2e83..faa7fde2ae86 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInput-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInput-test.js @@ -3109,4 +3109,106 @@ describe('ReactDOMInput', () => { expect(log).toEqual(['']); expect(node.value).toBe('a'); }); + + it('should not perform unnecessary DOM mutations for unchanged inputs', async () => { + // Regression test for https://github.com/facebook/react/issues/36229 + // When updating a parent that contains many inputs, only the input whose + // props actually changed should get DOM writes. + function App({value}) { + return ( +