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.
Sebastian Markbåge committed
Apr 9, 2023 at 18:06 UTC
e5146cb5250be1a4e66511af91549859b36ed488
11 files changed
+299
-267
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+102
-46
@@ -7,8 +7,6 @@
7
* @flow
8
*/
9
10
-import type {InputWithWrapperState} from './ReactDOMInput';
11
-
10
import {
11
registrationNameDependencies,
12
possibleRegistrationNames,
@@ -17,6 +15,7 @@ import {
15
import {canUseDOM} from 'shared/ExecutionEnvironment';
16
import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
17
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
18
+import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
19
20
import {
21
getValueForAttribute,
@@ -27,27 +26,24 @@ import {
26
setValueForNamespacedAttribute,
27
} from './DOMPropertyOperations';
28
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,
34
} from './ReactDOMInput';
35
+import {initOption, validateOptionProps} from './ReactDOMOption';
36
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,
41
} from './ReactDOMSelect';
42
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,
47
} from './ReactDOMTextarea';
48
import {track} from './inputValueTracking';
49
import setInnerHTML from './setInnerHTML';
@@ -79,6 +75,8 @@ import {
75
listenToNonDelegatedEvent,
76
} from '../events/DOMPluginEventSystem';
77
78
+let didWarnControlledToUncontrolled = false;
79
+let didWarnUncontrolledToControlled = false;
80
let didWarnInvalidHydration = false;
81
let canDiffStyleForHydrationWarning;
82
if (__DEV__) {
@@ -805,7 +803,9 @@ export function setInitialProperties(
803
break;
804
}
805
case 'input': {
808
- ReactDOMInputInitWrapperState(domElement, props);
806
+ if (__DEV__) {
807
+ checkControlledValueProps('input', props);
808
+ }
809
// We listen to this event in case to ensure emulated bubble
810
// listeners still fire for the invalid event.
811
listenToNonDelegatedEvent('invalid', domElement);
@@ -834,10 +834,10 @@ export function setInitialProperties(
834
break;
835
}
836
case 'checked': {
837
- const node = ((domElement: any): InputWithWrapperState);
837
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 =
841
!!checked &&
842
typeof checked !== 'function' &&
843
checked !== 'symbol';
@@ -866,11 +866,14 @@ export function setInitialProperties(
866
// TODO: Make sure we check if this is still unmounted or do any clean
867
// up necessary since we never stop tracking anymore.
868
track((domElement: any));
869
- ReactDOMInputPostMountWrapper(domElement, props, false);
869
+ validateInputProps(domElement, props);
870
+ initInput(domElement, props, false);
871
return;
872
}
873
case 'select': {
873
- ReactDOMSelectInitWrapperState(domElement, props);
874
+ if (__DEV__) {
875
+ checkControlledValueProps('select', props);
876
+ }
877
// We listen to this event in case to ensure emulated bubble
878
// listeners still fire for the invalid event.
879
listenToNonDelegatedEvent('invalid', domElement);
@@ -893,11 +896,14 @@ export function setInitialProperties(
896
}
897
}
898
}
896
- ReactDOMSelectPostMountWrapper(domElement, props);
899
+ validateSelectProps(domElement, props);
900
+ initSelect(domElement, props);
901
return;
902
}
903
case 'textarea': {
900
- ReactDOMTextareaInitWrapperState(domElement, props);
904
+ if (__DEV__) {
905
+ checkControlledValueProps('textarea', props);
906
+ }
907
// We listen to this event in case to ensure emulated bubble
908
// listeners still fire for the invalid event.
909
listenToNonDelegatedEvent('invalid', domElement);
@@ -936,11 +942,12 @@ export function setInitialProperties(
942
// TODO: Make sure we check if this is still unmounted or do any clean
943
// up necessary since we never stop tracking anymore.
944
track((domElement: any));
939
- ReactDOMTextareaPostMountWrapper(domElement, props);
945
+ validateTextareaProps(domElement, props);
946
+ initTextarea(domElement, props);
947
return;
948
}
949
case 'option': {
943
- ReactDOMOptionValidateProps(domElement, props);
950
+ validateOptionProps(domElement, props);
951
for (const propKey in props) {
952
if (!props.hasOwnProperty(propKey)) {
953
continue;
@@ -963,7 +970,7 @@ export function setInitialProperties(
970
}
971
}
972
}
966
- ReactDOMOptionPostMountWrapper(domElement, props);
973
+ initOption(domElement, props);
974
return;
975
}
976
case 'dialog': {
@@ -1213,17 +1220,17 @@ export function updateProperties(
1220
// In the middle of an update, it is possible to have multiple checked.
1221
// When a checked radio tries to change name, browser makes another radio's checked false.
1222
if (nextProps.type === 'radio' && nextProps.name != null) {
1216
- ReactDOMInputUpdateChecked(domElement, nextProps);
1223
+ updateInputChecked(domElement, nextProps);
1224
}
1225
for (let i = 0; i < updatePayload.length; i += 2) {
1226
const propKey = updatePayload[i];
1227
const propValue = updatePayload[i + 1];
1228
switch (propKey) {
1229
case 'checked': {
1223
- const node = ((domElement: any): InputWithWrapperState);
1230
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 =
1234
!!checked &&
1235
typeof checked !== 'function' &&
1236
checked !== 'symbol';
@@ -1249,10 +1256,50 @@ export function updateProperties(
1256
}
1257
}
1258
}
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
+ }
1299
// Update the wrapper around inputs *after* updating props. This has to
1300
// happen after updating the rest of props. Otherwise HTML5 input validations
1301
// raise warnings and prevent the new value from being assigned.
1255
- ReactDOMInputUpdateWrapper(domElement, nextProps);
1302
+ updateInput(domElement, nextProps);
1303
return;
1304
}
1305
case 'select': {
@@ -1272,7 +1319,7 @@ export function updateProperties(
1319
}
1320
// <select> value update needs to occur after <option> children
1321
// reconciliation
1275
- ReactDOMSelectPostUpdateWrapper(domElement, nextProps);
1322
+ updateSelect(domElement, lastProps, nextProps);
1323
return;
1324
}
1325
case 'textarea': {
@@ -1303,7 +1350,7 @@ export function updateProperties(
1350
}
1351
}
1352
}
1306
- ReactDOMTextareaUpdateWrapper(domElement, nextProps);
1353
+ updateTextarea(domElement, nextProps);
1354
return;
1355
}
1356
case 'option': {
@@ -2263,38 +2310,47 @@ export function diffHydratedProperties(
2310
listenToNonDelegatedEvent('toggle', domElement);
2311
break;
2312
case 'input':
2266
- ReactDOMInputInitWrapperState(domElement, props);
2313
+ if (__DEV__) {
2314
+ checkControlledValueProps('input', props);
2315
+ }
2316
// We listen to this event in case to ensure emulated bubble
2317
// listeners still fire for the invalid event.
2318
listenToNonDelegatedEvent('invalid', domElement);
2319
// TODO: Make sure we check if this is still unmounted or do any clean
2320
// up necessary since we never stop tracking anymore.
2321
track((domElement: any));
2322
+ validateInputProps(domElement, props);
2323
// For input and textarea we current always set the value property at
2324
// post mount to force it to diverge from attributes. However, for
2325
// option and select we don't quite do the same thing and select
2326
// is not resilient to the DOM state changing so we don't do that here.
2327
// TODO: Consider not doing this for input and textarea.
2278
- ReactDOMInputPostMountWrapper(domElement, props, true);
2328
+ initInput(domElement, props, true);
2329
break;
2330
case 'option':
2281
- ReactDOMOptionValidateProps(domElement, props);
2331
+ validateOptionProps(domElement, props);
2332
break;
2333
case 'select':
2284
- ReactDOMSelectInitWrapperState(domElement, props);
2334
+ if (__DEV__) {
2335
+ checkControlledValueProps('select', props);
2336
+ }
2337
// We listen to this event in case to ensure emulated bubble
2338
// listeners still fire for the invalid event.
2339
listenToNonDelegatedEvent('invalid', domElement);
2340
+ validateSelectProps(domElement, props);
2341
break;
2342
case 'textarea':
2290
- ReactDOMTextareaInitWrapperState(domElement, props);
2343
+ if (__DEV__) {
2344
+ checkControlledValueProps('textarea', props);
2345
+ }
2346
// We listen to this event in case to ensure emulated bubble
2347
// listeners still fire for the invalid event.
2348
listenToNonDelegatedEvent('invalid', domElement);
2349
// TODO: Make sure we check if this is still unmounted or do any clean
2350
// up necessary since we never stop tracking anymore.
2351
track((domElement: any));
2297
- ReactDOMTextareaPostMountWrapper(domElement, props);
2352
+ validateTextareaProps(domElement, props);
2353
+ initTextarea(domElement, props);
2354
break;
2355
}
2356
@@ -2472,13 +2528,13 @@ export function restoreControlledState(
2528
): void {
2529
switch (tag) {
2530
case 'input':
2475
- ReactDOMInputRestoreControlledState(domElement, props);
2531
+ restoreControlledInputState(domElement, props);
2532
return;
2533
case 'textarea':
2478
- ReactDOMTextareaRestoreControlledState(domElement, props);
2534
+ restoreControlledTextareaState(domElement, props);
2535
return;
2536
case 'select':
2481
- ReactDOMSelectRestoreControlledState(domElement, props);
2537
+ restoreControlledSelectState(domElement, props);
2538
return;
2539
}
2540
}
packages/react-dom-bindings/src/client/ReactDOMInput.js
+68
-123
@@ -12,7 +12,6 @@ import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCur
12
13
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
14
import {getToStringValue, toString} from './ToStringValue';
15
-import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
15
import {updateValueIfChanged} from './inputValueTracking';
16
import getActiveElement from './getActiveElement';
17
import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
@@ -20,29 +19,8 @@ import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
19
20
import type {ToStringValue} from './ToStringValue';
21
23
-export type InputWithWrapperState = HTMLInputElement & {
24
- _wrapperState: {
25
- initialValue: ToStringValue,
26
- initialChecked: ?boolean,
27
- controlled?: boolean,
28
- ...
29
- },
30
- checked: boolean,
31
- value: string,
32
- defaultChecked: boolean,
33
- defaultValue: string,
34
- ...
35
-};
36
-
22
let didWarnValueDefaultValue = false;
23
let didWarnCheckedDefaultChecked = false;
39
-let didWarnControlledToUncontrolled = false;
40
-let didWarnUncontrolledToControlled = false;
41
-
42
-function isControlled(props: any) {
43
- const usesChecked = props.type === 'checkbox' || props.type === 'radio';
44
- return usesChecked ? props.checked != null : props.value != null;
45
-}
24
25
/**
26
* Implements an <input> host component that allows setting these optional
@@ -61,10 +39,11 @@ function isControlled(props: any) {
39
* See http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
40
*/
41
64
-export function initWrapperState(element: Element, props: Object) {
42
+export function validateInputProps(element: Element, props: Object) {
43
if (__DEV__) {
66
- checkControlledValueProps('input', props);
67
-
44
+ // Normally we check for undefined and null the same, but explicitly specifying both
45
+ // properties, at all is probably worth warning for. We could move this either direction
46
+ // and just make it ok to pass null or just check hasOwnProperty.
47
if (
48
props.checked !== undefined &&
49
props.defaultChecked !== undefined &&
@@ -100,98 +79,30 @@ export function initWrapperState(element: Element, props: Object) {
79
didWarnValueDefaultValue = true;
80
}
81
}
103
-
104
- const node = ((element: any): InputWithWrapperState);
105
- const defaultValue = props.defaultValue == null ? '' : props.defaultValue;
106
- const initialChecked =
107
- props.checked != null ? props.checked : props.defaultChecked;
108
- node._wrapperState = {
109
- initialChecked:
110
- typeof initialChecked !== 'function' &&
111
- typeof initialChecked !== 'symbol' &&
112
- !!initialChecked,
113
- initialValue: getToStringValue(
114
- props.value != null ? props.value : defaultValue,
115
- ),
116
- controlled: isControlled(props),
117
- };
82
}
83
120
-export function updateChecked(element: Element, props: Object) {
121
- const node = ((element: any): InputWithWrapperState);
84
+export function updateInputChecked(element: Element, props: Object) {
85
+ const node: HTMLInputElement = (element: any);
86
const checked = props.checked;
87
if (checked != null) {
88
node.checked = checked;
89
}
90
}
91
128
-export function updateWrapper(element: Element, props: Object) {
129
- const node = ((element: any): InputWithWrapperState);
130
- if (__DEV__) {
131
- const controlled = isControlled(props);
132
-
133
- if (
134
- !node._wrapperState.controlled &&
135
- controlled &&
136
- !didWarnUncontrolledToControlled
137
- ) {
138
- console.error(
139
- 'A component is changing an uncontrolled input to be controlled. ' +
140
- 'This is likely caused by the value changing from undefined to ' +
141
- 'a defined value, which should not happen. ' +
142
- 'Decide between using a controlled or uncontrolled input ' +
143
- 'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components',
144
- );
145
- didWarnUncontrolledToControlled = true;
146
- }
147
- if (
148
- node._wrapperState.controlled &&
149
- !controlled &&
150
- !didWarnControlledToUncontrolled
151
- ) {
152
- console.error(
153
- 'A component is changing a controlled input to be uncontrolled. ' +
154
- 'This is likely caused by the value changing from a defined to ' +
155
- 'undefined, which should not happen. ' +
156
- 'Decide between using a controlled or uncontrolled input ' +
157
- 'element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components',
158
- );
159
- didWarnControlledToUncontrolled = true;
160
- }
161
- }
162
-
163
- updateChecked(element, props);
92
+export function updateInput(element: Element, props: Object) {
93
+ const node: HTMLInputElement = (element: any);
94
95
const value = getToStringValue(props.value);
96
const type = props.type;
97
168
- if (value != null) {
169
- if (type === 'number') {
170
- if (
171
- // $FlowFixMe[incompatible-type]
172
- (value === 0 && node.value === '') ||
173
- // We explicitly want to coerce to number here if possible.
174
- // eslint-disable-next-line
175
- node.value != (value: any)
176
- ) {
177
- node.value = toString((value: any));
178
- }
179
- } else if (node.value !== toString((value: any))) {
180
- node.value = toString((value: any));
181
- }
182
- } else if (type === 'submit' || type === 'reset') {
183
- // Submit/reset inputs need the attribute removed completely to avoid
184
- // blank-text buttons.
185
- node.removeAttribute('value');
186
- return;
187
- }
188
-
98
if (disableInputAttributeSyncing) {
99
// When not syncing the value attribute, React only assigns a new value
100
// whenever the defaultValue React prop has changed. When not present,
101
// React does nothing
193
- if (props.hasOwnProperty('defaultValue')) {
102
+ if (props.defaultValue != null) {
103
setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
104
+ } else {
105
+ node.removeAttribute('value');
106
}
107
} else {
108
// When syncing the value attribute, the value comes from a cascade of
@@ -199,10 +110,12 @@ export function updateWrapper(element: Element, props: Object) {
110
// 1. The value React property
111
// 2. The defaultValue React property
112
// 3. Otherwise there should be no change
202
- if (props.hasOwnProperty('value')) {
113
+ if (props.value != null) {
114
setDefaultValue(node, props.type, value);
204
- } else if (props.hasOwnProperty('defaultValue')) {
115
+ } else if (props.defaultValue != null) {
116
setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
117
+ } else {
118
+ node.removeAttribute('value');
119
}
120
}
121
@@ -222,18 +135,39 @@ export function updateWrapper(element: Element, props: Object) {
135
node.defaultChecked = !!props.defaultChecked;
136
}
137
}
138
+
139
+ updateInputChecked(element, props);
140
+
141
+ if (value != null) {
142
+ if (type === 'number') {
143
+ if (
144
+ // $FlowFixMe[incompatible-type]
145
+ (value === 0 && node.value === '') ||
146
+ // We explicitly want to coerce to number here if possible.
147
+ // eslint-disable-next-line
148
+ node.value != (value: any)
149
+ ) {
150
+ node.value = toString((value: any));
151
+ }
152
+ } else if (node.value !== toString((value: any))) {
153
+ node.value = toString((value: any));
154
+ }
155
+ } else if (type === 'submit' || type === 'reset') {
156
+ // Submit/reset inputs need the attribute removed completely to avoid
157
+ // blank-text buttons.
158
+ node.removeAttribute('value');
159
+ return;
160
+ }
161
}
162
227
-export function postMountWrapper(
163
+export function initInput(
164
element: Element,
165
props: Object,
166
isHydrating: boolean,
167
) {
232
- const node = ((element: any): InputWithWrapperState);
168
+ const node: HTMLInputElement = (element: any);
169
234
- // Do not assign value if it is already set. This prevents user text input
235
- // from being lost during SSR hydration.
236
- if (props.hasOwnProperty('value') || props.hasOwnProperty('defaultValue')) {
170
+ if (props.value != null || props.defaultValue != null) {
171
const type = props.type;
172
const isButton = type === 'submit' || type === 'reset';
173
@@ -243,7 +177,14 @@ export function postMountWrapper(
177
return;
178
}
179
246
- const initialValue = toString(node._wrapperState.initialValue);
180
+ const defaultValue =
181
+ props.defaultValue != null
182
+ ? toString(getToStringValue(props.defaultValue))
183
+ : '';
184
+ const initialValue =
185
+ props.value != null
186
+ ? toString(getToStringValue(props.value))
187
+ : defaultValue;
188
189
// Do not assign value if it is already set. This prevents user text input
190
// from being lost during SSR hydration.
@@ -282,9 +223,8 @@ export function postMountWrapper(
223
if (disableInputAttributeSyncing) {
224
// When not syncing the value attribute, assign the value attribute
225
// directly from the defaultValue React property (when present)
285
- const defaultValue = getToStringValue(props.defaultValue);
286
- if (defaultValue != null) {
287
- node.defaultValue = toString(defaultValue);
226
+ if (props.defaultValue != null) {
227
+ node.defaultValue = defaultValue;
228
}
229
} else {
230
// Otherwise, the value attribute is synchronized to the property,
@@ -304,20 +244,27 @@ export function postMountWrapper(
244
node.name = '';
245
}
246
247
+ const defaultChecked =
248
+ props.checked != null ? props.checked : props.defaultChecked;
249
+ const initialChecked =
250
+ typeof defaultChecked !== 'function' &&
251
+ typeof defaultChecked !== 'symbol' &&
252
+ !!defaultChecked;
253
+
254
// The checked property never gets assigned. It must be manually set.
255
// We don't want to do this when hydrating so that existing user input isn't
256
// modified
257
// TODO: I'm pretty sure this is a bug because initialValueTracking won't be
258
// correct for the hydration case then.
259
if (!isHydrating) {
313
- node.checked = !!node._wrapperState.initialChecked;
260
+ node.checked = !!initialChecked;
261
}
262
263
if (disableInputAttributeSyncing) {
264
// Only assign the checked attribute if it is defined. This saves
265
// a DOM write when controlling the checked attribute isn't needed
266
// (text inputs, submit/reset)
320
- if (props.hasOwnProperty('defaultChecked')) {
267
+ if (props.defaultChecked != null) {
268
node.defaultChecked = !node.defaultChecked;
269
node.defaultChecked = !!props.defaultChecked;
270
}
@@ -329,7 +276,7 @@ export function postMountWrapper(
276
// 2. The defaultChecked React property when present
277
// 3. Otherwise, false
278
node.defaultChecked = !node.defaultChecked;
332
- node.defaultChecked = !!node._wrapperState.initialChecked;
279
+ node.defaultChecked = !!initialChecked;
280
}
281
282
if (name !== '') {
@@ -337,13 +284,13 @@ export function postMountWrapper(
284
}
285
}
286
340
-export function restoreControlledState(element: Element, props: Object) {
341
- const node = ((element: any): InputWithWrapperState);
342
- updateWrapper(node, props);
287
+export function restoreControlledInputState(element: Element, props: Object) {
288
+ const node: HTMLInputElement = (element: any);
289
+ updateInput(node, props);
290
updateNamedCousins(node, props);
291
}
292
346
-function updateNamedCousins(rootNode: InputWithWrapperState, props: any) {
293
+function updateNamedCousins(rootNode: HTMLInputElement, props: any) {
294
const name = props.name;
295
if (props.type === 'radio' && name != null) {
296
let queryRoot: Element = rootNode;
@@ -391,7 +338,7 @@ function updateNamedCousins(rootNode: InputWithWrapperState, props: any) {
338
// If this is a controlled radio button group, forcing the input that
339
// was previously checked to update will cause it to be come re-checked
340
// as appropriate.
394
- updateWrapper(otherNode, otherProps);
341
+ updateInput(otherNode, otherProps);
342
}
343
}
344
}
@@ -405,7 +352,7 @@ function updateNamedCousins(rootNode: InputWithWrapperState, props: any) {
352
//
353
// https://github.com/facebook/react/issues/7253
354
export function setDefaultValue(
408
- node: InputWithWrapperState,
355
+ node: HTMLInputElement,
356
type: ?string,
357
value: ToStringValue,
358
) {
@@ -414,9 +361,7 @@ export function setDefaultValue(
361
type !== 'number' ||
362
getActiveElement(node.ownerDocument) !== node
363
) {
417
- if (value == null) {
418
- node.defaultValue = toString(node._wrapperState.initialValue);
419
- } else if (node.defaultValue !== toString(value)) {
364
+ if (node.defaultValue !== toString(value)) {
365
node.defaultValue = toString(value);
366
}
367
}
packages/react-dom-bindings/src/client/ReactDOMOption.js
+2
-2
@@ -18,7 +18,7 @@ let didWarnInvalidInnerHTML = false;
18
* Implements an <option> host component that warns when `selected` is set.
19
*/
20
21
-export function validateProps(element: Element, props: Object) {
21
+export function validateOptionProps(element: Element, props: Object) {
22
if (__DEV__) {
23
// If a value is not provided, then the children must be simple.
24
if (props.value == null) {
@@ -60,7 +60,7 @@ export function validateProps(element: Element, props: Object) {
60
}
61
}
62
63
-export function postMountWrapper(element: Element, props: Object) {
63
+export function initOption(element: Element, props: Object) {
64
// value="" should make a value attribute (#6219)
65
if (props.value != null) {
66
element.setAttribute('value', toString(getToStringValue(props.value)));
packages/react-dom-bindings/src/client/ReactDOMSelect.js
+12
-24
@@ -10,7 +10,6 @@
10
// TODO: direct imports like some-package/src/* are bad. Fix me.
11
import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber';
12
13
-import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
13
import {getToStringValue, toString} from './ToStringValue';
14
import isArray from 'shared/isArray';
15
@@ -20,10 +19,6 @@ if (__DEV__) {
19
didWarnValueDefaultValue = false;
20
}
21
23
-type SelectWithWrapperState = HTMLSelectElement & {
24
- _wrapperState: {wasMultiple: boolean},
25
-};
26
-
22
function getDeclarationErrorAddendum() {
23
const ownerName = getCurrentFiberOwnerNameInDevOrNull();
24
if (ownerName) {
@@ -39,8 +34,6 @@ const valuePropNames = ['value', 'defaultValue'];
34
*/
35
function checkSelectPropTypes(props: any) {
36
if (__DEV__) {
42
- checkControlledValueProps('select', props);
43
-
37
for (let i = 0; i < valuePropNames.length; i++) {
38
const propName = valuePropNames[i];
39
if (props[propName] == null) {
@@ -129,17 +122,9 @@ function updateOptions(
122
* selected.
123
*/
124
132
-export function initWrapperState(element: Element, props: Object) {
133
- const node = ((element: any): SelectWithWrapperState);
125
+export function validateSelectProps(element: Element, props: Object) {
126
if (__DEV__) {
127
checkSelectPropTypes(props);
136
- }
137
-
138
- node._wrapperState = {
139
- wasMultiple: !!props.multiple,
140
- };
141
-
142
- if (__DEV__) {
128
if (
129
props.value !== undefined &&
130
props.defaultValue !== undefined &&
@@ -157,8 +142,8 @@ export function initWrapperState(element: Element, props: Object) {
142
}
143
}
144
160
-export function postMountWrapper(element: Element, props: Object) {
161
- const node = ((element: any): SelectWithWrapperState);
145
+export function initSelect(element: Element, props: Object) {
146
+ const node: HTMLSelectElement = (element: any);
147
node.multiple = !!props.multiple;
148
const value = props.value;
149
if (value != null) {
@@ -168,10 +153,13 @@ export function postMountWrapper(element: Element, props: Object) {
153
}
154
}
155
171
-export function postUpdateWrapper(element: Element, props: Object) {
172
- const node = ((element: any): SelectWithWrapperState);
173
- const wasMultiple = node._wrapperState.wasMultiple;
174
- node._wrapperState.wasMultiple = !!props.multiple;
156
+export function updateSelect(
157
+ element: Element,
158
+ prevProps: Object,
159
+ props: Object,
160
+) {
161
+ const node: HTMLSelectElement = (element: any);
162
+ const wasMultiple = !!prevProps.multiple;
163
164
const value = props.value;
165
if (value != null) {
@@ -187,8 +175,8 @@ export function postUpdateWrapper(element: Element, props: Object) {
175
}
176
}
177
190
-export function restoreControlledState(element: Element, props: Object) {
191
- const node = ((element: any): SelectWithWrapperState);
178
+export function restoreControlledSelectState(element: Element, props: Object) {
179
+ const node: HTMLSelectElement = (element: any);
180
const value = props.value;
181
182
if (value != null) {
packages/react-dom-bindings/src/client/ReactDOMTextarea.js
+41
-45
@@ -9,18 +9,12 @@
9
10
import isArray from 'shared/isArray';
11
12
-import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
12
import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber';
13
import {getToStringValue, toString} from './ToStringValue';
15
-import type {ToStringValue} from './ToStringValue';
14
import {disableTextareaChildren} from 'shared/ReactFeatureFlags';
15
16
let didWarnValDefaultVal = false;
17
20
-export type TextAreaWithWrapperState = HTMLTextAreaElement & {
21
- _wrapperState: {initialValue: ToStringValue},
22
-};
23
-
18
/**
19
* Implements a <textarea> host component that allows setting `value`, and
20
* `defaultValue`. This differs from the traditional DOM API because value is
@@ -37,10 +31,8 @@ export type TextAreaWithWrapperState = HTMLTextAreaElement & {
31
* `defaultValue` if specified, or the children content (deprecated).
32
*/
33
40
-export function initWrapperState(element: Element, props: Object) {
41
- const node = ((element: any): TextAreaWithWrapperState);
34
+export function validateTextareaProps(element: Element, props: Object) {
35
if (__DEV__) {
43
- checkControlledValueProps('textarea', props);
36
if (
37
props.value !== undefined &&
38
props.defaultValue !== undefined &&
@@ -57,7 +49,41 @@ export function initWrapperState(element: Element, props: Object) {
49
);
50
didWarnValDefaultVal = true;
51
}
52
+ if (props.children != null && props.value == null) {
53
+ console.error(
54
+ 'Use the `defaultValue` or `value` props instead of setting ' +
55
+ 'children on <textarea>.',
56
+ );
57
+ }
58
+ }
59
+}
60
+
61
+export function updateTextarea(element: Element, props: Object) {
62
+ const node: HTMLTextAreaElement = (element: any);
63
+ const value = getToStringValue(props.value);
64
+ const defaultValue = getToStringValue(props.defaultValue);
65
+ if (defaultValue != null) {
66
+ node.defaultValue = toString(defaultValue);
67
+ } else {
68
+ node.defaultValue = '';
69
+ }
70
+ if (value != null) {
71
+ // Cast `value` to a string to ensure the value is set correctly. While
72
+ // browsers typically do this as necessary, jsdom doesn't.
73
+ const newValue = toString(value);
74
+ // To avoid side effects (such as losing text selection), only set value if changed
75
+ if (newValue !== node.value) {
76
+ node.value = newValue;
77
+ }
78
+ // TOOO: This should respect disableInputAttributeSyncing flag.
79
+ if (props.defaultValue == null && node.defaultValue !== newValue) {
80
+ node.defaultValue = newValue;
81
+ }
82
}
83
+}
84
+
85
+export function initTextarea(element: Element, props: Object) {
86
+ const node: HTMLTextAreaElement = (element: any);
87
88
let initialValue = props.value;
89
@@ -65,12 +91,6 @@ export function initWrapperState(element: Element, props: Object) {
91
if (initialValue == null) {
92
let {children, defaultValue} = props;
93
if (children != null) {
68
- if (__DEV__) {
69
- console.error(
70
- 'Use the `defaultValue` or `value` props instead of setting ' +
71
- 'children on <textarea>.',
72
- );
73
- }
94
if (!disableTextareaChildren) {
95
if (defaultValue != null) {
96
throw new Error(
@@ -97,34 +117,7 @@ export function initWrapperState(element: Element, props: Object) {
117
118
const stringValue = getToStringValue(initialValue);
119
node.defaultValue = (stringValue: any); // This will be toString:ed.
100
- node._wrapperState = {
101
- initialValue: stringValue,
102
- };
103
-}
104
-
105
-export function updateWrapper(element: Element, props: Object) {
106
- const node = ((element: any): TextAreaWithWrapperState);
107
- const value = getToStringValue(props.value);
108
- const defaultValue = getToStringValue(props.defaultValue);
109
- if (value != null) {
110
- // Cast `value` to a string to ensure the value is set correctly. While
111
- // browsers typically do this as necessary, jsdom doesn't.
112
- const newValue = toString(value);
113
- // To avoid side effects (such as losing text selection), only set value if changed
114
- if (newValue !== node.value) {
115
- node.value = newValue;
116
- }
117
- if (props.defaultValue == null && node.defaultValue !== newValue) {
118
- node.defaultValue = newValue;
119
- }
120
- }
121
- if (defaultValue != null) {
122
- node.defaultValue = toString(defaultValue);
123
- }
124
-}
120
126
-export function postMountWrapper(element: Element, props: Object) {
127
- const node = ((element: any): TextAreaWithWrapperState);
121
// This is in postMount because we need access to the DOM node, which is not
122
// available until after the component has mounted.
123
const textContent = node.textContent;
@@ -133,14 +126,17 @@ export function postMountWrapper(element: Element, props: Object) {
126
// initial value. In IE10/IE11 there is a bug where the placeholder attribute
127
// will populate textContent as well.
128
// https://developer.microsoft.com/microsoft-edge/platform/issues/101525/
136
- if (textContent === node._wrapperState.initialValue) {
129
+ if (textContent === stringValue) {
130
if (textContent !== '' && textContent !== null) {
131
node.value = textContent;
132
}
133
}
134
}
135
143
-export function restoreControlledState(element: Element, props: Object) {
136
+export function restoreControlledTextareaState(
137
+ element: Element,
138
+ props: Object,
139
+) {
140
// DOM component is still mounted; update
145
- updateWrapper(element, props);
141
+ updateTextarea(element, props);
142
}
packages/react-dom-bindings/src/client/inputValueTracking.js
-1
@@ -123,7 +123,6 @@ export function track(node: ElementWithValueTracker) {
123
return;
124
}
125
126
- // TODO: Once it's just Fiber we can move this to node._wrapperState
126
node._valueTracker = trackValueOnNode(node);
127
}
128
packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js
+13
-8
@@ -263,16 +263,17 @@ function getTargetInstForInputOrChangeEvent(
263
}
264
}
265
266
-function handleControlledInputBlur(node: HTMLInputElement) {
267
- const state = (node: any)._wrapperState;
268
-
269
- if (!state || !state.controlled || node.type !== 'number') {
266
+function handleControlledInputBlur(node: HTMLInputElement, props: any) {
267
+ if (node.type !== 'number') {
268
return;
269
}
270
271
if (!disableInputAttributeSyncing) {
274
- // If controlled, assign the value attribute to the current value on blur
275
- setDefaultValue((node: any), 'number', (node: any).value);
272
+ const isControlled = props.value != null;
273
+ if (isControlled) {
274
+ // If controlled, assign the value attribute to the current value on blur
275
+ setDefaultValue((node: any), 'number', (node: any).value);
276
+ }
277
}
278
}
279
@@ -335,8 +336,12 @@ function extractEvents(
336
}
337
338
// When blurring, set the value attribute for number inputs
338
- if (domEventName === 'focusout') {
339
- handleControlledInputBlur(((targetNode: any): HTMLInputElement));
339
+ if (domEventName === 'focusout' && targetInst) {
340
+ // These props aren't necessarily the most current but we warn for changing
341
+ // between controlled and uncontrolled, so it doesn't matter and the previous
342
+ // code was also broken for changes.
343
+ const props = targetInst.memoizedProps;
344
+ handleControlledInputBlur(((targetNode: any): HTMLInputElement), props);
345
}
346
}
347
packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
+1
-5
@@ -1166,11 +1166,7 @@ describe('DOMPropertyOperations', () => {
1166
).toErrorDev(
1167
'A component is changing a controlled input to be uncontrolled',
1168
);
1169
- if (disableInputAttributeSyncing) {
1170
- expect(container.firstChild.hasAttribute('value')).toBe(false);
1171
- } else {
1172
- expect(container.firstChild.getAttribute('value')).toBe('foo');
1173
- }
1169
+ expect(container.firstChild.hasAttribute('value')).toBe(false);
1170
expect(container.firstChild.value).toBe('foo');
1171
});
1172
packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+10
-3
@@ -1083,10 +1083,17 @@ describe('ReactDOMComponent', () => {
1083
);
1084
expect(nodeValueSetter).toHaveBeenCalledTimes(1);
1085
1086
- ReactDOM.render(
1087
- <input type="checkbox" onChange={onChange} checked={false} />,
1088
- container,
1086
+ expect(() => {
1087
+ ReactDOM.render(
1088
+ <input type="checkbox" onChange={onChange} checked={false} />,
1089
+ container,
1090
+ );
1091
+ }).toErrorDev(
1092
+ ' A component is changing an uncontrolled input to be controlled. This is likely caused by ' +
1093
+ 'the value changing from undefined to a defined value, which should not happen. Decide between ' +
1094
+ 'using a controlled or uncontrolled input element for the lifetime of the component.',
1095
);
1096
+
1097
// TODO: Non-null values are updated twice on inputs. This is should ideally be fixed.
1098
expect(nodeValueSetter).toHaveBeenCalledTimes(3);
1099
packages/react-dom/src/__tests__/ReactDOMInput-test.js
+28
-10
@@ -1952,11 +1952,7 @@ describe('ReactDOMInput', () => {
1952
expect(renderInputWithStringThenWithUndefined).toErrorDev(
1953
'A component is changing a controlled input to be uncontrolled.',
1954
);
1955
- if (disableInputAttributeSyncing) {
1956
- expect(input.getAttribute('value')).toBe(null);
1957
- } else {
1958
- expect(input.getAttribute('value')).toBe('first');
1959
- }
1955
+ expect(input.getAttribute('value')).toBe(null);
1956
});
1957
1958
it('preserves the value property', () => {
@@ -2002,11 +1998,7 @@ describe('ReactDOMInput', () => {
1998
'or `undefined` for uncontrolled components.',
1999
'A component is changing a controlled input to be uncontrolled.',
2000
]);
2005
- if (disableInputAttributeSyncing) {
2006
- expect(input.hasAttribute('value')).toBe(false);
2007
- } else {
2008
- expect(input.getAttribute('value')).toBe('first');
2009
- }
2001
+ expect(input.hasAttribute('value')).toBe(false);
2002
});
2003
2004
it('preserves the value property', () => {
@@ -2165,4 +2157,30 @@ describe('ReactDOMInput', () => {
2157
expect(node.hasAttribute('value')).toBe(false);
2158
});
2159
});
2160
+
2161
+ it('should remove previous `defaultValue`', () => {
2162
+ const node = ReactDOM.render(
2163
+ <input type="text" defaultValue="0" />,
2164
+ container,
2165
+ );
2166
+
2167
+ expect(node.value).toBe('0');
2168
+ expect(node.defaultValue).toBe('0');
2169
+
2170
+ ReactDOM.render(<input type="text" />, container);
2171
+ expect(node.defaultValue).toBe('');
2172
+ });
2173
+
2174
+ it('should treat `defaultValue={null}` as missing', () => {
2175
+ const node = ReactDOM.render(
2176
+ <input type="text" defaultValue="0" />,
2177
+ container,
2178
+ );
2179
+
2180
+ expect(node.value).toBe('0');
2181
+ expect(node.defaultValue).toBe('0');
2182
+
2183
+ ReactDOM.render(<input type="text" defaultValue={null} />, container);
2184
+ expect(node.defaultValue).toBe('');
2185
+ });
2186
});
packages/react-dom/src/__tests__/ReactDOMTextarea-test.js
+22
@@ -736,4 +736,26 @@ describe('ReactDOMTextarea', () => {
736
expect(node.value).toBe('foo');
737
});
738
});
739
+
740
+ it('should remove previous `defaultValue`', () => {
741
+ const container = document.createElement('div');
742
+ const node = ReactDOM.render(<textarea defaultValue="0" />, container);
743
+
744
+ expect(node.value).toBe('0');
745
+ expect(node.defaultValue).toBe('0');
746
+
747
+ ReactDOM.render(<textarea />, container);
748
+ expect(node.defaultValue).toBe('');
749
+ });
750
+
751
+ it('should treat `defaultValue={null}` as missing', () => {
752
+ const container = document.createElement('div');
753
+ const node = ReactDOM.render(<textarea defaultValue="0" />, container);
754
+
755
+ expect(node.value).toBe('0');
756
+ expect(node.defaultValue).toBe('0');
757
+
758
+ ReactDOM.render(<textarea defaultValue={null} />, container);
759
+ expect(node.defaultValue).toBe('');
760
+ });
761
});