@samitouri / QOS-React-2 / commits / 6b90976bc1

Use already extracted values instead of reading off props for controlled components (#26596)

Since `props.x` is a possibly megamorphic access, it can be slow to access and trigger recompilation. When we are looping over the props and pattern matching every key, anyway, we've already done this work. We can just reuse the same value by stashing it outside the loop in the stack. This only makes sense for updates in diffInCommitPhase since otherwise we don't have the full set of props in that loop. We also have to be careful not to skip over equal values since we need to extract them anyway.

Sebastian Markbåge committed Apr 11, 2023 at 13:32 UTC 6b90976bc10f325146b193286435a4b5015ef605
4 files changed +378 -154
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+279 -71
@@ -28,7 +28,6 @@ import {
28 import {
29 validateInputProps,
30 initInput,
31 - updateInputChecked,
31 updateInput,
32 restoreControlledInputState,
33 } from './ReactDOMInput';
@@ -834,6 +833,12 @@ export function setInitialProperties(
833 // We listen to this event in case to ensure emulated bubble
834 // listeners still fire for the invalid event.
835 listenToNonDelegatedEvent('invalid', domElement);
836 +
837 + let type = null;
838 + let value = null;
839 + let defaultValue = null;
840 + let checked = null;
841 + let defaultChecked = null;
842 for (const propKey in props) {
843 if (!props.hasOwnProperty(propKey)) {
844 continue;
@@ -851,6 +856,7 @@ export function setInitialProperties(
856 typeof propValue !== 'symbol' &&
857 typeof propValue !== 'boolean'
858 ) {
859 + type = propValue;
860 if (__DEV__) {
861 checkAttributeStringCoercion(propValue, propKey);
862 }
@@ -859,17 +865,26 @@ export function setInitialProperties(
865 break;
866 }
867 case 'checked': {
862 - const checked =
868 + checked = propValue;
869 + const checkedValue =
870 propValue != null ? propValue : props.defaultChecked;
871 const inputElement: HTMLInputElement = (domElement: any);
872 inputElement.checked =
866 - !!checked &&
867 - typeof checked !== 'function' &&
868 - checked !== 'symbol';
873 + !!checkedValue &&
874 + typeof checkedValue !== 'function' &&
875 + checkedValue !== 'symbol';
876 + break;
877 + }
878 + case 'defaultChecked': {
879 + defaultChecked = propValue;
880 break;
881 }
882 case 'value': {
872 - // This is handled by updateWrapper below.
883 + value = propValue;
884 + break;
885 + }
886 + case 'defaultValue': {
887 + defaultValue = propValue;
888 break;
889 }
890 case 'children':
@@ -882,7 +897,6 @@ export function setInitialProperties(
897 }
898 break;
899 }
885 - // defaultChecked and defaultValue are ignored by setProp
900 default: {
901 setProp(domElement, tag, propKey, propValue, props, null);
902 }
@@ -892,7 +906,15 @@ export function setInitialProperties(
906 // up necessary since we never stop tracking anymore.
907 track((domElement: any));
908 validateInputProps(domElement, props);
895 - initInput(domElement, props, false);
909 + initInput(
910 + domElement,
911 + value,
912 + defaultValue,
913 + checked,
914 + defaultChecked,
915 + type,
916 + false,
917 + );
918 return;
919 }
920 case 'select': {
@@ -902,6 +924,9 @@ export function setInitialProperties(
924 // We listen to this event in case to ensure emulated bubble
925 // listeners still fire for the invalid event.
926 listenToNonDelegatedEvent('invalid', domElement);
927 + let value = null;
928 + let defaultValue = null;
929 + let multiple = null;
930 for (const propKey in props) {
931 if (!props.hasOwnProperty(propKey)) {
932 continue;
@@ -912,17 +937,28 @@ export function setInitialProperties(
937 }
938 switch (propKey) {
939 case 'value': {
915 - // This is handled by updateWrapper below.
940 + value = propValue;
941 + // This is handled by initSelect below.
942 break;
943 }
918 - // defaultValue are ignored by setProp
944 + case 'defaultValue': {
945 + defaultValue = propValue;
946 + // This is handled by initSelect below.
947 + break;
948 + }
949 + case 'multiple': {
950 + multiple = propValue;
951 + // TODO: We don't actually have to fall through here because we set it
952 + // in initSelect anyway. We can remove the special case in setProp.
953 + }
954 + // Fallthrough
955 default: {
956 setProp(domElement, tag, propKey, propValue, props, null);
957 }
958 }
959 }
960 validateSelectProps(domElement, props);
925 - initSelect(domElement, props);
961 + initSelect(domElement, value, defaultValue, multiple);
962 return;
963 }
964 case 'textarea': {
@@ -932,6 +968,9 @@ export function setInitialProperties(
968 // We listen to this event in case to ensure emulated bubble
969 // listeners still fire for the invalid event.
970 listenToNonDelegatedEvent('invalid', domElement);
971 + let value = null;
972 + let defaultValue = null;
973 + let children = null;
974 for (const propKey in props) {
975 if (!props.hasOwnProperty(propKey)) {
976 continue;
@@ -942,11 +981,17 @@ export function setInitialProperties(
981 }
982 switch (propKey) {
983 case 'value': {
945 - // This is handled by updateWrapper below.
984 + value = propValue;
985 + // This is handled by initTextarea below.
986 + break;
987 + }
988 + case 'defaultValue': {
989 + defaultValue = propValue;
990 break;
991 }
992 case 'children': {
949 - // TODO: Handled by initWrapperState above.
993 + children = propValue;
994 + // Handled by initTextarea above.
995 break;
996 }
997 case 'dangerouslySetInnerHTML': {
@@ -958,7 +1003,6 @@ export function setInitialProperties(
1003 }
1004 break;
1005 }
961 - // defaultValue is ignored by setProp
1006 default: {
1007 setProp(domElement, tag, propKey, propValue, props);
1008 }
@@ -968,7 +1012,7 @@ export function setInitialProperties(
1012 // up necessary since we never stop tracking anymore.
1013 track((domElement: any));
1014 validateTextareaProps(domElement, props);
971 - initTextarea(domElement, props);
1015 + initTextarea(domElement, value, defaultValue, children);
1016 return;
1017 }
1018 case 'option': {
@@ -1249,12 +1293,12 @@ export function updateProperties(
1293 break;
1294 }
1295 case 'input': {
1252 - // Update checked *before* name.
1253 - // In the middle of an update, it is possible to have multiple checked.
1254 - // When a checked radio tries to change name, browser makes another radio's checked false.
1255 - if (nextProps.type === 'radio' && nextProps.name != null) {
1256 - updateInputChecked(domElement, nextProps);
1257 - }
1296 + let name = null;
1297 + let type = null;
1298 + let value = null;
1299 + let defaultValue = null;
1300 + let checked = null;
1301 + let defaultChecked = null;
1302 for (const propKey in lastProps) {
1303 const lastProp = lastProps[propKey];
1304 if (
@@ -1264,12 +1308,12 @@ export function updateProperties(
1308 ) {
1309 switch (propKey) {
1310 case 'checked': {
1267 - const checked = nextProps.defaultChecked;
1311 + const checkedValue = nextProps.defaultChecked;
1312 const inputElement: HTMLInputElement = (domElement: any);
1313 inputElement.checked =
1270 - !!checked &&
1271 - typeof checked !== 'function' &&
1272 - checked !== 'symbol';
1314 + !!checkedValue &&
1315 + typeof checkedValue !== 'function' &&
1316 + checkedValue !== 'symbol';
1317 break;
1318 }
1319 case 'value': {
@@ -1288,22 +1332,56 @@ export function updateProperties(
1332 const lastProp = lastProps[propKey];
1333 if (
1334 nextProps.hasOwnProperty(propKey) &&
1291 - nextProp !== lastProp &&
1335 (nextProp != null || lastProp != null)
1336 ) {
1337 switch (propKey) {
1338 + case 'type': {
1339 + type = nextProp;
1340 + // Fast path since 'type' is very common on inputs
1341 + if (nextProp !== lastProp) {
1342 + if (
1343 + nextProp != null &&
1344 + typeof nextProp !== 'function' &&
1345 + typeof nextProp !== 'symbol' &&
1346 + typeof nextProp !== 'boolean'
1347 + ) {
1348 + if (__DEV__) {
1349 + checkAttributeStringCoercion(nextProp, propKey);
1350 + }
1351 + domElement.setAttribute(propKey, nextProp);
1352 + } else {
1353 + domElement.removeAttribute(propKey);
1354 + }
1355 + }
1356 + break;
1357 + }
1358 + case 'name': {
1359 + name = nextProp;
1360 + break;
1361 + }
1362 case 'checked': {
1296 - const checked =
1297 - nextProp != null ? nextProp : nextProps.defaultChecked;
1298 - const inputElement: HTMLInputElement = (domElement: any);
1299 - inputElement.checked =
1300 - !!checked &&
1301 - typeof checked !== 'function' &&
1302 - checked !== 'symbol';
1363 + checked = nextProp;
1364 + if (nextProp !== lastProp) {
1365 + const checkedValue =
1366 + nextProp != null ? nextProp : nextProps.defaultChecked;
1367 + const inputElement: HTMLInputElement = (domElement: any);
1368 + inputElement.checked =
1369 + !!checkedValue &&
1370 + typeof checkedValue !== 'function' &&
1371 + checkedValue !== 'symbol';
1372 + }
1373 + break;
1374 + }
1375 + case 'defaultChecked': {
1376 + defaultChecked = nextProp;
1377 break;
1378 }
1379 case 'value': {
1306 - // This is handled by updateWrapper below.
1380 + value = nextProp;
1381 + break;
1382 + }
1383 + case 'defaultValue': {
1384 + defaultValue = nextProp;
1385 break;
1386 }
1387 case 'children':
@@ -1316,9 +1394,16 @@ export function updateProperties(
1394 }
1395 break;
1396 }
1319 - // defaultChecked and defaultValue are ignored by setProp
1397 default: {
1321 - setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1398 + if (nextProp !== lastProp)
1399 + setProp(
1400 + domElement,
1401 + tag,
1402 + propKey,
1403 + nextProp,
1404 + nextProps,
1405 + lastProp,
1406 + );
1407 }
1408 }
1409 }
@@ -1363,28 +1448,59 @@ export function updateProperties(
1448 didWarnControlledToUncontrolled = true;
1449 }
1450 }
1451 +
1452 + // Update checked *before* name.
1453 + // In the middle of an update, it is possible to have multiple checked.
1454 + // When a checked radio tries to change name, browser makes another radio's checked false.
1455 + if (
1456 + name != null &&
1457 + typeof name !== 'function' &&
1458 + typeof name !== 'symbol' &&
1459 + typeof name !== 'boolean'
1460 + ) {
1461 + if (__DEV__) {
1462 + checkAttributeStringCoercion(name, 'name');
1463 + }
1464 + domElement.setAttribute('name', name);
1465 + } else {
1466 + domElement.removeAttribute('name');
1467 + }
1468 +
1469 // Update the wrapper around inputs *after* updating props. This has to
1470 // happen after updating the rest of props. Otherwise HTML5 input validations
1471 // raise warnings and prevent the new value from being assigned.
1369 - updateInput(domElement, nextProps);
1472 + updateInput(
1473 + domElement,
1474 + value,
1475 + defaultValue,
1476 + checked,
1477 + defaultChecked,
1478 + type,
1479 + );
1480 return;
1481 }
1482 case 'select': {
1483 + let value = null;
1484 + let defaultValue = null;
1485 + let multiple = null;
1486 + let wasMultiple = null;
1487 for (const propKey in lastProps) {
1488 const lastProp = lastProps[propKey];
1375 - if (
1376 - lastProps.hasOwnProperty(propKey) &&
1377 - lastProp != null &&
1378 - !nextProps.hasOwnProperty(propKey)
1379 - ) {
1489 + if (lastProps.hasOwnProperty(propKey) && lastProp != null) {
1490 switch (propKey) {
1491 case 'value': {
1492 // This is handled by updateWrapper below.
1493 break;
1494 }
1495 // defaultValue are ignored by setProp
1496 + case 'multiple': {
1497 + wasMultiple = lastProp;
1498 + // TODO: Move special case in here from setProp.
1499 + }
1500 + // Fallthrough
1501 default: {
1387 - setProp(domElement, tag, propKey, null, nextProps, lastProp);
1502 + if (!nextProps.hasOwnProperty(propKey))
1503 + setProp(domElement, tag, propKey, null, nextProps, lastProp);
1504 }
1505 }
1506 }
@@ -1394,27 +1510,45 @@ export function updateProperties(
1510 const lastProp = lastProps[propKey];
1511 if (
1512 nextProps.hasOwnProperty(propKey) &&
1397 - nextProp !== lastProp &&
1513 (nextProp != null || lastProp != null)
1514 ) {
1515 switch (propKey) {
1516 case 'value': {
1402 - // This is handled by updateWrapper below.
1517 + value = nextProp;
1518 + // This is handled by updateSelect below.
1519 break;
1520 }
1405 - // defaultValue are ignored by setProp
1521 + case 'defaultValue': {
1522 + defaultValue = nextProp;
1523 + break;
1524 + }
1525 + case 'multiple': {
1526 + multiple = nextProp;
1527 + // TODO: Just move the special case in here from setProp.
1528 + }
1529 + // Fallthrough
1530 default: {
1407 - setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1531 + if (nextProp !== lastProp)
1532 + setProp(
1533 + domElement,
1534 + tag,
1535 + propKey,
1536 + nextProp,
1537 + nextProps,
1538 + lastProp,
1539 + );
1540 }
1541 }
1542 }
1543 }
1544 // <select> value update needs to occur after <option> children
1545 // reconciliation
1414 - updateSelect(domElement, lastProps, nextProps);
1546 + updateSelect(domElement, value, defaultValue, multiple, wasMultiple);
1547 return;
1548 }
1549 case 'textarea': {
1550 + let value = null;
1551 + let defaultValue = null;
1552 for (const propKey in lastProps) {
1553 const lastProp = lastProps[propKey];
1554 if (
@@ -1424,7 +1558,7 @@ export function updateProperties(
1558 ) {
1559 switch (propKey) {
1560 case 'value': {
1427 - // This is handled by updateWrapper below.
1561 + // This is handled by updateTextarea below.
1562 break;
1563 }
1564 case 'children': {
@@ -1443,12 +1577,16 @@ export function updateProperties(
1577 const lastProp = lastProps[propKey];
1578 if (
1579 nextProps.hasOwnProperty(propKey) &&
1446 - nextProp !== lastProp &&
1580 (nextProp != null || lastProp != null)
1581 ) {
1582 switch (propKey) {
1583 case 'value': {
1451 - // This is handled by updateWrapper below.
1584 + value = nextProp;
1585 + // This is handled by updateTextarea below.
1586 + break;
1587 + }
1588 + case 'defaultValue': {
1589 + defaultValue = nextProp;
1590 break;
1591 }
1592 case 'children': {
@@ -1464,14 +1602,21 @@ export function updateProperties(
1602 }
1603 break;
1604 }
1467 - // defaultValue is ignored by setProp
1605 default: {
1469 - setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1606 + if (nextProp !== lastProp)
1607 + setProp(
1608 + domElement,
1609 + tag,
1610 + propKey,
1611 + nextProp,
1612 + nextProps,
1613 + lastProp,
1614 + );
1615 }
1616 }
1617 }
1618 }
1474 - updateTextarea(domElement, nextProps);
1619 + updateTextarea(domElement, value, defaultValue);
1620 return;
1621 }
1622 case 'option': {
@@ -1660,28 +1805,53 @@ export function updatePropertiesWithDiff(
1805 break;
1806 }
1807 case 'input': {
1663 - // Update checked *before* name.
1664 - // In the middle of an update, it is possible to have multiple checked.
1665 - // When a checked radio tries to change name, browser makes another radio's checked false.
1666 - if (nextProps.type === 'radio' && nextProps.name != null) {
1667 - updateInputChecked(domElement, nextProps);
1668 - }
1808 + const name = nextProps.name;
1809 + const type = nextProps.type;
1810 + const value = nextProps.value;
1811 + const defaultValue = nextProps.defaultValue;
1812 + const checked = nextProps.checked;
1813 + const defaultChecked = nextProps.defaultChecked;
1814 for (let i = 0; i < updatePayload.length; i += 2) {
1815 const propKey = updatePayload[i];
1816 const propValue = updatePayload[i + 1];
1817 switch (propKey) {
1818 + case 'type': {
1819 + // Fast path since 'type' is very common on inputs
1820 + if (
1821 + propValue != null &&
1822 + typeof propValue !== 'function' &&
1823 + typeof propValue !== 'symbol' &&
1824 + typeof propValue !== 'boolean'
1825 + ) {
1826 + if (__DEV__) {
1827 + checkAttributeStringCoercion(propValue, propKey);
1828 + }
1829 + domElement.setAttribute(propKey, propValue);
1830 + } else {
1831 + domElement.removeAttribute(propKey);
1832 + }
1833 + break;
1834 + }
1835 + case 'name': {
1836 + break;
1837 + }
1838 case 'checked': {
1674 - const checked =
1839 + const checkedValue =
1840 propValue != null ? propValue : nextProps.defaultChecked;
1841 const inputElement: HTMLInputElement = (domElement: any);
1842 inputElement.checked =
1678 - !!checked &&
1679 - typeof checked !== 'function' &&
1680 - checked !== 'symbol';
1843 + !!checkedValue &&
1844 + typeof checkedValue !== 'function' &&
1845 + checkedValue !== 'symbol';
1846 + break;
1847 + }
1848 + case 'defaultChecked': {
1849 break;
1850 }
1851 case 'value': {
1684 - // This is handled by updateWrapper below.
1852 + break;
1853 + }
1854 + case 'defaultValue': {
1855 break;
1856 }
1857 case 'children':
@@ -1694,7 +1864,6 @@ export function updatePropertiesWithDiff(
1864 }
1865 break;
1866 }
1697 - // defaultChecked and defaultValue are ignored by setProp
1867 default: {
1868 setProp(domElement, tag, propKey, propValue, nextProps, null);
1869 }
@@ -1740,13 +1909,42 @@ export function updatePropertiesWithDiff(
1909 didWarnControlledToUncontrolled = true;
1910 }
1911 }
1912 +
1913 + // Update checked *before* name.
1914 + // In the middle of an update, it is possible to have multiple checked.
1915 + // When a checked radio tries to change name, browser makes another radio's checked false.
1916 + if (
1917 + name != null &&
1918 + typeof name !== 'function' &&
1919 + typeof name !== 'symbol' &&
1920 + typeof name !== 'boolean'
1921 + ) {
1922 + if (__DEV__) {
1923 + checkAttributeStringCoercion(name, 'name');
1924 + }
1925 + domElement.setAttribute('name', name);
1926 + } else {
1927 + domElement.removeAttribute('name');
1928 + }
1929 +
1930 // Update the wrapper around inputs *after* updating props. This has to
1931 // happen after updating the rest of props. Otherwise HTML5 input validations
1932 // raise warnings and prevent the new value from being assigned.
1746 - updateInput(domElement, nextProps);
1933 + updateInput(
1934 + domElement,
1935 + value,
1936 + defaultValue,
1937 + checked,
1938 + defaultChecked,
1939 + type,
1940 + );
1941 return;
1942 }
1943 case 'select': {
1944 + const value = nextProps.value;
1945 + const defaultValue = nextProps.defaultValue;
1946 + const multiple = nextProps.multiple;
1947 + const wasMultiple = lastProps.multiple;
1948 for (let i = 0; i < updatePayload.length; i += 2) {
1949 const propKey = updatePayload[i];
1950 const propValue = updatePayload[i + 1];
@@ -1763,10 +1961,12 @@ export function updatePropertiesWithDiff(
1961 }
1962 // <select> value update needs to occur after <option> children
1963 // reconciliation
1766 - updateSelect(domElement, lastProps, nextProps);
1964 + updateSelect(domElement, value, defaultValue, multiple, wasMultiple);
1965 return;
1966 }
1967 case 'textarea': {
1968 + const value = nextProps.value;
1969 + const defaultValue = nextProps.defaultValue;
1970 for (let i = 0; i < updatePayload.length; i += 2) {
1971 const propKey = updatePayload[i];
1972 const propValue = updatePayload[i + 1];
@@ -1794,7 +1994,7 @@ export function updatePropertiesWithDiff(
1994 }
1995 }
1996 }
1797 - updateTextarea(domElement, nextProps);
1997 + updateTextarea(domElement, value, defaultValue);
1998 return;
1999 }
2000 case 'option': {
@@ -2770,7 +2970,15 @@ export function diffHydratedProperties(
2970 // option and select we don't quite do the same thing and select
2971 // is not resilient to the DOM state changing so we don't do that here.
2972 // TODO: Consider not doing this for input and textarea.
2773 - initInput(domElement, props, true);
2973 + initInput(
2974 + domElement,
2975 + props.value,
2976 + props.defaultValue,
2977 + props.checked,
2978 + props.defaultChecked,
2979 + props.type,
2980 + true,
2981 + );
2982 break;
2983 case 'option':
2984 validateOptionProps(domElement, props);
@@ -2795,7 +3003,7 @@ export function diffHydratedProperties(
3003 // up necessary since we never stop tracking anymore.
3004 track((domElement: any));
3005 validateTextareaProps(domElement, props);
2798 - initTextarea(domElement, props);
3006 + initTextarea(domElement, props.value, props.defaultValue, props.children);
3007 break;
3008 }
3009
packages/react-dom-bindings/src/client/ReactDOMInput.js
+64 -58
@@ -81,26 +81,22 @@ export function validateInputProps(element: Element, props: Object) {
81 }
82 }
83
84 -export function updateInputChecked(element: Element, props: Object) {
85 - const node: HTMLInputElement = (element: any);
86 - const checked = props.checked;
87 - if (checked != null && node.checked !== !!checked) {
88 - node.checked = checked;
89 - }
90 -}
91 -
92 -export function updateInput(element: Element, props: Object) {
84 +export function updateInput(
85 + element: Element,
86 + value: ?string,
87 + defaultValue: ?string,
88 + checked: ?boolean,
89 + defaultChecked: ?boolean,
90 + type: ?string,
91 +) {
92 const node: HTMLInputElement = (element: any);
93
95 - const value = getToStringValue(props.value);
96 - const type = props.type;
97 -
94 if (disableInputAttributeSyncing) {
95 // When not syncing the value attribute, React only assigns a new value
96 // whenever the defaultValue React prop has changed. When not present,
97 // React does nothing
102 - if (props.defaultValue != null) {
103 - setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
98 + if (defaultValue != null) {
99 + setDefaultValue(node, type, getToStringValue(defaultValue));
100 } else {
101 node.removeAttribute('value');
102 }
@@ -110,10 +106,10 @@ export function updateInput(element: Element, props: Object) {
106 // 1. The value React property
107 // 2. The defaultValue React property
108 // 3. Otherwise there should be no change
113 - if (props.value != null) {
114 - setDefaultValue(node, props.type, value);
115 - } else if (props.defaultValue != null) {
116 - setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
109 + if (value != null) {
110 + setDefaultValue(node, type, getToStringValue(value));
111 + } else if (defaultValue != null) {
112 + setDefaultValue(node, type, getToStringValue(defaultValue));
113 } else {
114 node.removeAttribute('value');
115 }
@@ -123,20 +119,22 @@ export function updateInput(element: Element, props: Object) {
119 // When not syncing the checked attribute, the attribute is directly
120 // controllable from the defaultValue React property. It needs to be
121 // updated as new props come in.
126 - if (props.defaultChecked == null) {
122 + if (defaultChecked == null) {
123 node.removeAttribute('checked');
124 } else {
129 - node.defaultChecked = !!props.defaultChecked;
125 + node.defaultChecked = !!defaultChecked;
126 }
127 } else {
128 // When syncing the checked attribute, it only changes when it needs
129 // to be removed, such as transitioning from a checkbox into a text input
134 - if (props.checked == null && props.defaultChecked != null) {
135 - node.defaultChecked = !!props.defaultChecked;
130 + if (checked == null && defaultChecked != null) {
131 + node.defaultChecked = !!defaultChecked;
132 }
133 }
134
139 - updateInputChecked(element, props);
135 + if (checked != null && node.checked !== !!checked) {
136 + node.checked = checked;
137 + }
138
139 if (value != null) {
140 if (type === 'number') {
@@ -147,10 +145,10 @@ export function updateInput(element: Element, props: Object) {
145 // eslint-disable-next-line
146 node.value != (value: any)
147 ) {
150 - node.value = toString((value: any));
148 + node.value = toString(getToStringValue(value));
149 }
152 - } else if (node.value !== toString((value: any))) {
153 - node.value = toString((value: any));
150 + } else if (node.value !== toString(getToStringValue(value))) {
151 + node.value = toString(getToStringValue(value));
152 }
153 } else if (type === 'submit' || type === 'reset') {
154 // Submit/reset inputs need the attribute removed completely to avoid
@@ -162,36 +160,33 @@ export function updateInput(element: Element, props: Object) {
160
161 export function initInput(
162 element: Element,
165 - props: Object,
163 + value: ?string,
164 + defaultValue: ?string,
165 + checked: ?boolean,
166 + defaultChecked: ?boolean,
167 + type: ?string,
168 isHydrating: boolean,
169 ) {
170 const node: HTMLInputElement = (element: any);
171
170 - if (props.value != null || props.defaultValue != null) {
171 - const type = props.type;
172 + if (value != null || defaultValue != null) {
173 const isButton = type === 'submit' || type === 'reset';
174
175 // Avoid setting value attribute on submit/reset inputs as it overrides the
176 // default value provided by the browser. See: #12872
176 - if (isButton && (props.value === undefined || props.value === null)) {
177 + if (isButton && (value === undefined || value === null)) {
178 return;
179 }
180
180 - const defaultValue =
181 - props.defaultValue != null
182 - ? toString(getToStringValue(props.defaultValue))
183 - : '';
181 + const defaultValueStr =
182 + defaultValue != null ? toString(getToStringValue(defaultValue)) : '';
183 const initialValue =
185 - props.value != null
186 - ? toString(getToStringValue(props.value))
187 - : defaultValue;
184 + value != null ? toString(getToStringValue(value)) : defaultValueStr;
185
186 // Do not assign value if it is already set. This prevents user text input
187 // from being lost during SSR hydration.
188 if (!isHydrating) {
189 if (disableInputAttributeSyncing) {
193 - const value = getToStringValue(props.value);
194 -
190 // When not syncing the value attribute, the value property points
191 // directly to the React prop. Only assign it if it exists.
192 if (value != null) {
@@ -203,8 +198,8 @@ export function initInput(
198 // prematurely marking required inputs as invalid. Equality is compared
199 // to the current value in case the browser provided value is not an
200 // empty string.
206 - if (isButton || value !== node.value) {
207 - node.value = toString(value);
201 + if (isButton || toString(getToStringValue(value)) !== node.value) {
202 + node.value = toString(getToStringValue(value));
203 }
204 }
205 } else {
@@ -223,8 +218,8 @@ export function initInput(
218 if (disableInputAttributeSyncing) {
219 // When not syncing the value attribute, assign the value attribute
220 // directly from the defaultValue React property (when present)
226 - if (props.defaultValue != null) {
227 - node.defaultValue = defaultValue;
221 + if (defaultValue != null) {
222 + node.defaultValue = defaultValueStr;
223 }
224 } else {
225 // Otherwise, the value attribute is synchronized to the property,
@@ -244,12 +239,13 @@ export function initInput(
239 node.name = '';
240 }
241
247 - const defaultChecked =
248 - props.checked != null ? props.checked : props.defaultChecked;
242 + const checkedOrDefault = checked != null ? checked : defaultChecked;
243 + // TODO: This 'function' or 'symbol' check isn't replicated in other places
244 + // so this semantic is inconsistent.
245 const initialChecked =
250 - typeof defaultChecked !== 'function' &&
251 - typeof defaultChecked !== 'symbol' &&
252 - !!defaultChecked;
246 + typeof checkedOrDefault !== 'function' &&
247 + typeof checkedOrDefault !== 'symbol' &&
248 + !!checkedOrDefault;
249
250 // The checked property never gets assigned. It must be manually set.
251 // We don't want to do this when hydrating so that existing user input isn't
@@ -264,9 +260,9 @@ export function initInput(
260 // Only assign the checked attribute if it is defined. This saves
261 // a DOM write when controlling the checked attribute isn't needed
262 // (text inputs, submit/reset)
267 - if (props.defaultChecked != null) {
263 + if (defaultChecked != null) {
264 node.defaultChecked = !node.defaultChecked;
269 - node.defaultChecked = !!props.defaultChecked;
265 + node.defaultChecked = !!defaultChecked;
266 }
267 } else {
268 // When syncing the checked attribute, both the checked property and
@@ -285,12 +281,15 @@ export function initInput(
281 }
282
283 export function restoreControlledInputState(element: Element, props: Object) {
288 - const node: HTMLInputElement = (element: any);
289 - updateInput(node, props);
290 - updateNamedCousins(node, props);
291 -}
292 -
293 -function updateNamedCousins(rootNode: HTMLInputElement, props: any) {
284 + const rootNode: HTMLInputElement = (element: any);
285 + updateInput(
286 + rootNode,
287 + props.value,
288 + props.defaultValue,
289 + props.checked,
290 + props.defaultChecked,
291 + props.type,
292 + );
293 const name = props.name;
294 if (props.type === 'radio' && name != null) {
295 let queryRoot: Element = rootNode;
@@ -322,7 +321,7 @@ function updateNamedCousins(rootNode: HTMLInputElement, props: any) {
321 // and the same name are rendered into the same form (same as #1939).
322 // That's probably okay; we don't support it just as we don't support
323 // mixing React radio buttons with non-React ones.
325 - const otherProps = getFiberCurrentPropsFromNode(otherNode);
324 + const otherProps: any = getFiberCurrentPropsFromNode(otherNode);
325
326 if (!otherProps) {
327 throw new Error(
@@ -338,7 +337,14 @@ function updateNamedCousins(rootNode: HTMLInputElement, props: any) {
337 // If this is a controlled radio button group, forcing the input that
338 // was previously checked to update will cause it to be come re-checked
339 // as appropriate.
341 - updateInput(otherNode, otherProps);
340 + updateInput(
341 + otherNode,
342 + otherProps.value,
343 + otherProps.defaultValue,
344 + otherProps.checked,
345 + otherProps.defaultChecked,
346 + otherProps.type,
347 + );
348 }
349 }
350 }
packages/react-dom-bindings/src/client/ReactDOMSelect.js
+19 -15
@@ -142,35 +142,39 @@ export function validateSelectProps(element: Element, props: Object) {
142 }
143 }
144
145 -export function initSelect(element: Element, props: Object) {
145 +export function initSelect(
146 + element: Element,
147 + value: ?string,
148 + defaultValue: ?string,
149 + multiple: ?boolean,
150 +) {
151 const node: HTMLSelectElement = (element: any);
147 - node.multiple = !!props.multiple;
148 - const value = props.value;
152 + node.multiple = !!multiple;
153 if (value != null) {
150 - updateOptions(node, !!props.multiple, value, false);
151 - } else if (props.defaultValue != null) {
152 - updateOptions(node, !!props.multiple, props.defaultValue, true);
154 + updateOptions(node, !!multiple, value, false);
155 + } else if (defaultValue != null) {
156 + updateOptions(node, !!multiple, defaultValue, true);
157 }
158 }
159
160 export function updateSelect(
161 element: Element,
158 - prevProps: Object,
159 - props: Object,
162 + value: ?string,
163 + defaultValue: ?string,
164 + multiple: ?boolean,
165 + wasMultiple: ?boolean,
166 ) {
167 const node: HTMLSelectElement = (element: any);
162 - const wasMultiple = !!prevProps.multiple;
168
164 - const value = props.value;
169 if (value != null) {
166 - updateOptions(node, !!props.multiple, value, false);
167 - } else if (wasMultiple !== !!props.multiple) {
170 + updateOptions(node, !!multiple, value, false);
171 + } else if (!!wasMultiple !== !!multiple) {
172 // For simplicity, reapply `defaultValue` if `multiple` is toggled.
169 - if (props.defaultValue != null) {
170 - updateOptions(node, !!props.multiple, props.defaultValue, true);
173 + if (defaultValue != null) {
174 + updateOptions(node, !!multiple, defaultValue, true);
175 } else {
176 // Revert the select back to its default unselected state.
173 - updateOptions(node, !!props.multiple, props.multiple ? [] : '', false);
177 + updateOptions(node, !!multiple, multiple ? [] : '', false);
178 }
179 }
180 }
packages/react-dom-bindings/src/client/ReactDOMTextarea.js
+16 -10
@@ -58,41 +58,47 @@ export function validateTextareaProps(element: Element, props: Object) {
58 }
59 }
60
61 -export function updateTextarea(element: Element, props: Object) {
61 +export function updateTextarea(
62 + element: Element,
63 + value: ?string,
64 + defaultValue: ?string,
65 +) {
66 const node: HTMLTextAreaElement = (element: any);
63 - const value = getToStringValue(props.value);
67 if (value != null) {
68 // Cast `value` to a string to ensure the value is set correctly. While
69 // browsers typically do this as necessary, jsdom doesn't.
67 - const newValue = toString(value);
70 + const newValue = toString(getToStringValue(value));
71 // To avoid side effects (such as losing text selection), only set value if changed
72 if (newValue !== node.value) {
73 node.value = newValue;
74 }
75 // TOOO: This should respect disableInputAttributeSyncing flag.
73 - if (props.defaultValue == null) {
76 + if (defaultValue == null) {
77 if (node.defaultValue !== newValue) {
78 node.defaultValue = newValue;
79 }
80 return;
81 }
82 }
80 - const defaultValue = getToStringValue(props.defaultValue);
83 if (defaultValue != null) {
82 - node.defaultValue = toString(defaultValue);
84 + node.defaultValue = toString(getToStringValue(defaultValue));
85 } else {
86 node.defaultValue = '';
87 }
88 }
89
88 -export function initTextarea(element: Element, props: Object) {
90 +export function initTextarea(
91 + element: Element,
92 + value: ?string,
93 + defaultValue: ?string,
94 + children: ?string,
95 +) {
96 const node: HTMLTextAreaElement = (element: any);
97
91 - let initialValue = props.value;
98 + let initialValue = value;
99
100 // Only bother fetching default value if we're going to use it
101 if (initialValue == null) {
95 - let {children, defaultValue} = props;
102 if (children != null) {
103 if (!disableTextareaChildren) {
104 if (defaultValue != null) {
@@ -141,5 +147,5 @@ export function restoreControlledTextareaState(
147 props: Object,
148 ) {
149 // DOM component is still mounted; update
144 - updateTextarea(element, props);
150 + updateTextarea(element, props.value, props.defaultValue);
151 }