Skip to content

Commit e5146cb

Browse files
authored
Refactor some controlled component stuff (#26573)
This is mainly renaming some stuff. The behavior change is hasOwnProperty to nullish check. I had a bigger refactor that was a dead-end but might as well land this part and see if I can pick it up later.
1 parent 657698e commit e5146cb

11 files changed

+299
-267
lines changed

packages/react-dom-bindings/src/client/ReactDOMComponent.js

Lines changed: 102 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
* @flow
88
*/
99

10-
import type {InputWithWrapperState} from './ReactDOMInput';
11-
1210
import {
1311
registrationNameDependencies,
1412
possibleRegistrationNames,
@@ -17,6 +15,7 @@ import {
1715
import {canUseDOM} from 'shared/ExecutionEnvironment';
1816
import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
1917
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
18+
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
2019

2120
import {
2221
getValueForAttribute,
@@ -27,27 +26,24 @@ import {
2726
setValueForNamespacedAttribute,
2827
} from './DOMPropertyOperations';
2928
import {
30-
initWrapperState as ReactDOMInputInitWrapperState,
31-
postMountWrapper as ReactDOMInputPostMountWrapper,
32-
updateChecked as ReactDOMInputUpdateChecked,
33-
updateWrapper as ReactDOMInputUpdateWrapper,
34-
restoreControlledState as ReactDOMInputRestoreControlledState,
29+
validateInputProps,
30+
initInput,
31+
updateInputChecked,
32+
updateInput,
33+
restoreControlledInputState,
3534
} from './ReactDOMInput';
35+
import {initOption, validateOptionProps} from './ReactDOMOption';
3636
import {
37-
postMountWrapper as ReactDOMOptionPostMountWrapper,
38-
validateProps as ReactDOMOptionValidateProps,
39-
} from './ReactDOMOption';
40-
import {
41-
initWrapperState as ReactDOMSelectInitWrapperState,
42-
postMountWrapper as ReactDOMSelectPostMountWrapper,
43-
restoreControlledState as ReactDOMSelectRestoreControlledState,
44-
postUpdateWrapper as ReactDOMSelectPostUpdateWrapper,
37+
validateSelectProps,
38+
initSelect,
39+
restoreControlledSelectState,
40+
updateSelect,
4541
} from './ReactDOMSelect';
4642
import {
47-
initWrapperState as ReactDOMTextareaInitWrapperState,
48-
postMountWrapper as ReactDOMTextareaPostMountWrapper,
49-
updateWrapper as ReactDOMTextareaUpdateWrapper,
50-
restoreControlledState as ReactDOMTextareaRestoreControlledState,
43+
validateTextareaProps,
44+
initTextarea,
45+
updateTextarea,
46+
restoreControlledTextareaState,
5147
} from './ReactDOMTextarea';
5248
import {track} from './inputValueTracking';
5349
import setInnerHTML from './setInnerHTML';
@@ -79,6 +75,8 @@ import {
7975
listenToNonDelegatedEvent,
8076
} from '../events/DOMPluginEventSystem';
8177

78+
let didWarnControlledToUncontrolled = false;
79+
let didWarnUncontrolledToControlled = false;
8280
let didWarnInvalidHydration = false;
8381
let canDiffStyleForHydrationWarning;
8482
if (__DEV__) {
@@ -805,7 +803,9 @@ export function setInitialProperties(
805803
break;
806804
}
807805
case 'input': {
808-
ReactDOMInputInitWrapperState(domElement, props);
806+
if (__DEV__) {
807+
checkControlledValueProps('input', props);
808+
}
809809
// We listen to this event in case to ensure emulated bubble
810810
// listeners still fire for the invalid event.
811811
listenToNonDelegatedEvent('invalid', domElement);
@@ -834,10 +834,10 @@ export function setInitialProperties(
834834
break;
835835
}
836836
case 'checked': {
837-
const node = ((domElement: any): InputWithWrapperState);
838837
const checked =
839-
propValue != null ? propValue : node._wrapperState.initialChecked;
840-
node.checked =
838+
propValue != null ? propValue : props.defaultChecked;
839+
const inputElement: HTMLInputElement = (domElement: any);
840+
inputElement.checked =
841841
!!checked &&
842842
typeof checked !== 'function' &&
843843
checked !== 'symbol';
@@ -866,11 +866,14 @@ export function setInitialProperties(
866866
// TODO: Make sure we check if this is still unmounted or do any clean
867867
// up necessary since we never stop tracking anymore.
868868
track((domElement: any));
869-
ReactDOMInputPostMountWrapper(domElement, props, false);
869+
validateInputProps(domElement, props);
870+
initInput(domElement, props, false);
870871
return;
871872
}
872873
case 'select': {
873-
ReactDOMSelectInitWrapperState(domElement, props);
874+
if (__DEV__) {
875+
checkControlledValueProps('select', props);
876+
}
874877
// We listen to this event in case to ensure emulated bubble
875878
// listeners still fire for the invalid event.
876879
listenToNonDelegatedEvent('invalid', domElement);
@@ -893,11 +896,14 @@ export function setInitialProperties(
893896
}
894897
}
895898
}
896-
ReactDOMSelectPostMountWrapper(domElement, props);
899+
validateSelectProps(domElement, props);
900+
initSelect(domElement, props);
897901
return;
898902
}
899903
case 'textarea': {
900-
ReactDOMTextareaInitWrapperState(domElement, props);
904+
if (__DEV__) {
905+
checkControlledValueProps('textarea', props);
906+
}
901907
// We listen to this event in case to ensure emulated bubble
902908
// listeners still fire for the invalid event.
903909
listenToNonDelegatedEvent('invalid', domElement);
@@ -936,11 +942,12 @@ export function setInitialProperties(
936942
// TODO: Make sure we check if this is still unmounted or do any clean
937943
// up necessary since we never stop tracking anymore.
938944
track((domElement: any));
939-
ReactDOMTextareaPostMountWrapper(domElement, props);
945+
validateTextareaProps(domElement, props);
946+
initTextarea(domElement, props);
940947
return;
941948
}
942949
case 'option': {
943-
ReactDOMOptionValidateProps(domElement, props);
950+
validateOptionProps(domElement, props);
944951
for (const propKey in props) {
945952
if (!props.hasOwnProperty(propKey)) {
946953
continue;
@@ -963,7 +970,7 @@ export function setInitialProperties(
963970
}
964971
}
965972
}
966-
ReactDOMOptionPostMountWrapper(domElement, props);
973+
initOption(domElement, props);
967974
return;
968975
}
969976
case 'dialog': {
@@ -1213,17 +1220,17 @@ export function updateProperties(
12131220
// In the middle of an update, it is possible to have multiple checked.
12141221
// When a checked radio tries to change name, browser makes another radio's checked false.
12151222
if (nextProps.type === 'radio' && nextProps.name != null) {
1216-
ReactDOMInputUpdateChecked(domElement, nextProps);
1223+
updateInputChecked(domElement, nextProps);
12171224
}
12181225
for (let i = 0; i < updatePayload.length; i += 2) {
12191226
const propKey = updatePayload[i];
12201227
const propValue = updatePayload[i + 1];
12211228
switch (propKey) {
12221229
case 'checked': {
1223-
const node = ((domElement: any): InputWithWrapperState);
12241230
const checked =
1225-
propValue != null ? propValue : node._wrapperState.initialChecked;
1226-
node.checked =
1231+
propValue != null ? propValue : nextProps.defaultChecked;
1232+
const inputElement: HTMLInputElement = (domElement: any);
1233+
inputElement.checked =
12271234
!!checked &&
12281235
typeof checked !== 'function' &&
12291236
checked !== 'symbol';
@@ -1249,10 +1256,50 @@ export function updateProperties(
12491256
}
12501257
}
12511258
}
1259+
1260+
if (__DEV__) {
1261+
const wasControlled =
1262+
lastProps.type === 'checkbox' || lastProps.type === 'radio'
1263+
? lastProps.checked != null
1264+
: lastProps.value != null;
1265+
const isControlled =
1266+
nextProps.type === 'checkbox' || nextProps.type === 'radio'
1267+
? nextProps.checked != null
1268+
: nextProps.value != null;
1269+
1270+
if (
1271+
!wasControlled &&
1272+
isControlled &&
1273+
!didWarnUncontrolledToControlled
1274+
) {
1275+
console.error(
1276+
'A component is changing an uncontrolled input to be controlled. ' +
1277+
'This is likely caused by the value changing from undefined to ' +
1278+
'a defined value, which should not happen. ' +
1279+
'Decide between using a controlled or uncontrolled input ' +
1280+
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components',
1281+
);
1282+
didWarnUncontrolledToControlled = true;
1283+
}
1284+
if (
1285+
wasControlled &&
1286+
!isControlled &&
1287+
!didWarnControlledToUncontrolled
1288+
) {
1289+
console.error(
1290+
'A component is changing a controlled input to be uncontrolled. ' +
1291+
'This is likely caused by the value changing from a defined to ' +
1292+
'undefined, which should not happen. ' +
1293+
'Decide between using a controlled or uncontrolled input ' +
1294+
'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components',
1295+
);
1296+
didWarnControlledToUncontrolled = true;
1297+
}
1298+
}
12521299
// Update the wrapper around inputs *after* updating props. This has to
12531300
// happen after updating the rest of props. Otherwise HTML5 input validations
12541301
// raise warnings and prevent the new value from being assigned.
1255-
ReactDOMInputUpdateWrapper(domElement, nextProps);
1302+
updateInput(domElement, nextProps);
12561303
return;
12571304
}
12581305
case 'select': {
@@ -1272,7 +1319,7 @@ export function updateProperties(
12721319
}
12731320
// <select> value update needs to occur after <option> children
12741321
// reconciliation
1275-
ReactDOMSelectPostUpdateWrapper(domElement, nextProps);
1322+
updateSelect(domElement, lastProps, nextProps);
12761323
return;
12771324
}
12781325
case 'textarea': {
@@ -1303,7 +1350,7 @@ export function updateProperties(
13031350
}
13041351
}
13051352
}
1306-
ReactDOMTextareaUpdateWrapper(domElement, nextProps);
1353+
updateTextarea(domElement, nextProps);
13071354
return;
13081355
}
13091356
case 'option': {
@@ -2263,38 +2310,47 @@ export function diffHydratedProperties(
22632310
listenToNonDelegatedEvent('toggle', domElement);
22642311
break;
22652312
case 'input':
2266-
ReactDOMInputInitWrapperState(domElement, props);
2313+
if (__DEV__) {
2314+
checkControlledValueProps('input', props);
2315+
}
22672316
// We listen to this event in case to ensure emulated bubble
22682317
// listeners still fire for the invalid event.
22692318
listenToNonDelegatedEvent('invalid', domElement);
22702319
// TODO: Make sure we check if this is still unmounted or do any clean
22712320
// up necessary since we never stop tracking anymore.
22722321
track((domElement: any));
2322+
validateInputProps(domElement, props);
22732323
// For input and textarea we current always set the value property at
22742324
// post mount to force it to diverge from attributes. However, for
22752325
// option and select we don't quite do the same thing and select
22762326
// is not resilient to the DOM state changing so we don't do that here.
22772327
// TODO: Consider not doing this for input and textarea.
2278-
ReactDOMInputPostMountWrapper(domElement, props, true);
2328+
initInput(domElement, props, true);
22792329
break;
22802330
case 'option':
2281-
ReactDOMOptionValidateProps(domElement, props);
2331+
validateOptionProps(domElement, props);
22822332
break;
22832333
case 'select':
2284-
ReactDOMSelectInitWrapperState(domElement, props);
2334+
if (__DEV__) {
2335+
checkControlledValueProps('select', props);
2336+
}
22852337
// We listen to this event in case to ensure emulated bubble
22862338
// listeners still fire for the invalid event.
22872339
listenToNonDelegatedEvent('invalid', domElement);
2340+
validateSelectProps(domElement, props);
22882341
break;
22892342
case 'textarea':
2290-
ReactDOMTextareaInitWrapperState(domElement, props);
2343+
if (__DEV__) {
2344+
checkControlledValueProps('textarea', props);
2345+
}
22912346
// We listen to this event in case to ensure emulated bubble
22922347
// listeners still fire for the invalid event.
22932348
listenToNonDelegatedEvent('invalid', domElement);
22942349
// TODO: Make sure we check if this is still unmounted or do any clean
22952350
// up necessary since we never stop tracking anymore.
22962351
track((domElement: any));
2297-
ReactDOMTextareaPostMountWrapper(domElement, props);
2352+
validateTextareaProps(domElement, props);
2353+
initTextarea(domElement, props);
22982354
break;
22992355
}
23002356

@@ -2472,13 +2528,13 @@ export function restoreControlledState(
24722528
): void {
24732529
switch (tag) {
24742530
case 'input':
2475-
ReactDOMInputRestoreControlledState(domElement, props);
2531+
restoreControlledInputState(domElement, props);
24762532
return;
24772533
case 'textarea':
2478-
ReactDOMTextareaRestoreControlledState(domElement, props);
2534+
restoreControlledTextareaState(domElement, props);
24792535
return;
24802536
case 'select':
2481-
ReactDOMSelectRestoreControlledState(domElement, props);
2537+
restoreControlledSelectState(domElement, props);
24822538
return;
24832539
}
24842540
}

0 commit comments

Comments
 (0)