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
}