Fix “no onChange handler” warning to fire on falsy values ("", 0, false) too (#12628)
* throw warning for falsey `value` prop * add nop onChange handler to tests for `value` prop * prettier * check for falsey checked * fix tests for `checked` prop * new tests for `value` prop * test formatting * forgot 0 (: * test for falsey `checked` prop * add null check * Update ReactDOMInput-test.js * revert unneeded change * prettier * Update DOMPropertyOperations-test.js * Update ReactDOMInput-test.js * Update ReactDOMSelect-test.js * Fixes and tests * Remove unnecessary changes
Nicole Levy committed
Jul 17, 2018 at 17:46 UTC
171e0b7d447a20b7cc90808a95478c6e5b8b6af0
5 files changed
+128
-36
packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
+1
-1
@@ -79,7 +79,7 @@ describe('DOMPropertyOperations', () => {
79
80
it('should not remove empty attributes for special properties', () => {
81
const container = document.createElement('div');
82
- ReactDOM.render(<input value="" />, container);
82
+ ReactDOM.render(<input value="" onChange={() => {}} />, container);
83
expect(container.firstChild.getAttribute('value')).toBe('');
84
expect(container.firstChild.value).toBe('');
85
});
packages/react-dom/src/__tests__/ReactDOMInput-test.js
+106
-30
@@ -47,6 +47,71 @@ describe('ReactDOMInput', () => {
47
document.body.removeChild(container);
48
});
49
50
+ it('should warn for controlled value of 0 with missing onChange', () => {
51
+ expect(() => {
52
+ ReactDOM.render(<input type="text" value={0} />, container);
53
+ }).toWarnDev(
54
+ 'Failed prop type: You provided a `value` prop to a form field without an `onChange` handler.',
55
+ );
56
+ });
57
+
58
+ it('should warn for controlled value of "" with missing onChange', () => {
59
+ expect(() => {
60
+ ReactDOM.render(<input type="text" value="" />, container);
61
+ }).toWarnDev(
62
+ 'Failed prop type: You provided a `value` prop to a form field without an `onChange` handler.',
63
+ );
64
+ });
65
+
66
+ it('should warn for controlled value of "0" with missing onChange', () => {
67
+ expect(() => {
68
+ ReactDOM.render(<input type="text" value="0" />, container);
69
+ }).toWarnDev(
70
+ 'Failed prop type: You provided a `value` prop to a form field without an `onChange` handler.',
71
+ );
72
+ });
73
+
74
+ it('should warn for controlled value of false with missing onChange', () => {
75
+ expect(() =>
76
+ ReactDOM.render(<input type="checkbox" checked={false} />, container),
77
+ ).toWarnDev(
78
+ 'Failed prop type: You provided a `checked` prop to a form field without an `onChange` handler.',
79
+ );
80
+ });
81
+
82
+ it('should warn with checked and no onChange handler with readOnly specified', () => {
83
+ ReactDOM.render(
84
+ <input type="checkbox" checked={false} readOnly={true} />,
85
+ container,
86
+ );
87
+ ReactDOM.unmountComponentAtNode(container);
88
+
89
+ expect(() =>
90
+ ReactDOM.render(
91
+ <input type="checkbox" checked={false} readOnly={false} />,
92
+ container,
93
+ ),
94
+ ).toWarnDev(
95
+ 'Failed prop type: You provided a `checked` prop to a form field without an `onChange` handler. ' +
96
+ 'This will render a read-only field. If the field should be mutable use `defaultChecked`. ' +
97
+ 'Otherwise, set either `onChange` or `readOnly`.',
98
+ );
99
+ });
100
+
101
+ it('should not warn about missing onChange in uncontrolled inputs', () => {
102
+ ReactDOM.render(<input />, container);
103
+ ReactDOM.unmountComponentAtNode(container);
104
+ ReactDOM.render(<input value={undefined} />, container);
105
+ ReactDOM.unmountComponentAtNode(container);
106
+ ReactDOM.render(<input type="text" />, container);
107
+ ReactDOM.unmountComponentAtNode(container);
108
+ ReactDOM.render(<input type="text" value={undefined} />, container);
109
+ ReactDOM.unmountComponentAtNode(container);
110
+ ReactDOM.render(<input type="checkbox" />, container);
111
+ ReactDOM.unmountComponentAtNode(container);
112
+ ReactDOM.render(<input type="checkbox" checked={undefined} />, container);
113
+ });
114
+
115
it('should properly control a value even if no event listener exists', () => {
116
let node;
117
@@ -452,7 +517,7 @@ describe('ReactDOMInput', () => {
517
});
518
519
it('should display `value` of number 0', () => {
455
- const stub = <input type="text" value={0} />;
520
+ const stub = <input type="text" value={0} onChange={emptyFunction} />;
521
const node = ReactDOM.render(stub, container);
522
523
expect(node.value).toBe('0');
@@ -590,8 +655,14 @@ describe('ReactDOMInput', () => {
655
});
656
657
it('should properly transition from an empty value to 0', function() {
593
- ReactDOM.render(<input type="text" value="" />, container);
594
- ReactDOM.render(<input type="text" value={0} />, container);
658
+ ReactDOM.render(
659
+ <input type="text" value="" onChange={emptyFunction} />,
660
+ container,
661
+ );
662
+ ReactDOM.render(
663
+ <input type="text" value={0} onChange={emptyFunction} />,
664
+ container,
665
+ );
666
667
const node = container.firstChild;
668
@@ -600,8 +671,14 @@ describe('ReactDOMInput', () => {
671
});
672
673
it('should properly transition from 0 to an empty value', function() {
603
- ReactDOM.render(<input type="text" value={0} />, container);
604
- ReactDOM.render(<input type="text" value="" />, container);
674
+ ReactDOM.render(
675
+ <input type="text" value={0} onChange={emptyFunction} />,
676
+ container,
677
+ );
678
+ ReactDOM.render(
679
+ <input type="text" value="" onChange={emptyFunction} />,
680
+ container,
681
+ );
682
683
const node = container.firstChild;
684
@@ -610,8 +687,14 @@ describe('ReactDOMInput', () => {
687
});
688
689
it('should properly transition a text input from 0 to an empty 0.0', function() {
613
- ReactDOM.render(<input type="text" value={0} />, container);
614
- ReactDOM.render(<input type="text" value="0.0" />, container);
690
+ ReactDOM.render(
691
+ <input type="text" value={0} onChange={emptyFunction} />,
692
+ container,
693
+ );
694
+ ReactDOM.render(
695
+ <input type="text" value="0.0" onChange={emptyFunction} />,
696
+ container,
697
+ );
698
699
const node = container.firstChild;
700
@@ -620,8 +703,14 @@ describe('ReactDOMInput', () => {
703
});
704
705
it('should properly transition a number input from "" to 0', function() {
623
- ReactDOM.render(<input type="number" value="" />, container);
624
- ReactDOM.render(<input type="number" value={0} />, container);
706
+ ReactDOM.render(
707
+ <input type="number" value="" onChange={emptyFunction} />,
708
+ container,
709
+ );
710
+ ReactDOM.render(
711
+ <input type="number" value={0} onChange={emptyFunction} />,
712
+ container,
713
+ );
714
715
const node = container.firstChild;
716
@@ -630,8 +719,14 @@ describe('ReactDOMInput', () => {
719
});
720
721
it('should properly transition a number input from "" to "0"', function() {
633
- ReactDOM.render(<input type="number" value="" />, container);
634
- ReactDOM.render(<input type="number" value="0" />, container);
722
+ ReactDOM.render(
723
+ <input type="number" value="" onChange={emptyFunction} />,
724
+ container,
725
+ );
726
+ ReactDOM.render(
727
+ <input type="number" value="0" onChange={emptyFunction} />,
728
+ container,
729
+ );
730
731
const node = container.firstChild;
732
@@ -874,25 +969,6 @@ describe('ReactDOMInput', () => {
969
dispatchEventOnNode(node, 'input');
970
});
971
877
- it('should warn with checked and no onChange handler with readOnly specified', () => {
878
- ReactDOM.render(
879
- <input type="checkbox" checked="false" readOnly={true} />,
880
- container,
881
- );
882
- ReactDOM.unmountComponentAtNode(container);
883
-
884
- expect(() =>
885
- ReactDOM.render(
886
- <input type="checkbox" checked="false" readOnly={false} />,
887
- container,
888
- ),
889
- ).toWarnDev(
890
- 'Failed prop type: You provided a `checked` prop to a form field without an `onChange` handler. ' +
891
- 'This will render a read-only field. If the field should be mutable use `defaultChecked`. ' +
892
- 'Otherwise, set either `onChange` or `readOnly`.',
893
- );
894
- });
895
-
972
it('should update defaultValue to empty string', () => {
973
ReactDOM.render(<input type="text" defaultValue={'foo'} />, container);
974
ReactDOM.render(<input type="text" defaultValue={''} />, container);
packages/react-dom/src/__tests__/ReactDOMSelect-test.js
+7
@@ -654,6 +654,13 @@ describe('ReactDOMSelect', () => {
654
);
655
});
656
657
+ it('should not warn about missing onChange in uncontrolled textareas', () => {
658
+ const container = document.createElement('div');
659
+ ReactDOM.render(<select />, container);
660
+ ReactDOM.unmountComponentAtNode(container);
661
+ ReactDOM.render(<select value={undefined} />, container);
662
+ });
663
+
664
it('should be able to safely remove select onChange', () => {
665
function changeView() {
666
ReactDOM.unmountComponentAtNode(container);
packages/react-dom/src/__tests__/ReactDOMTextarea-test.js
+8
-1
@@ -97,7 +97,7 @@ describe('ReactDOMTextarea', () => {
97
});
98
99
it('should display `value` of number 0', () => {
100
- const stub = <textarea value={0} />;
100
+ const stub = <textarea value={0} onChange={emptyFunction} />;
101
const node = renderTextarea(stub);
102
103
expect(node.value).toBe('0');
@@ -416,4 +416,11 @@ describe('ReactDOMTextarea', () => {
416
<textarea value="foo" defaultValue="bar" readOnly={true} />,
417
);
418
});
419
+
420
+ it('should not warn about missing onChange in uncontrolled textareas', () => {
421
+ const container = document.createElement('div');
422
+ ReactDOM.render(<textarea />, container);
423
+ ReactDOM.unmountComponentAtNode(container);
424
+ ReactDOM.render(<textarea value={undefined} />, container);
425
+ });
426
});
packages/react-dom/src/shared/ReactControlledValuePropTypes.js
+6
-4
@@ -30,11 +30,12 @@ if (__DEV__) {
30
const propTypes = {
31
value: function(props, propName, componentName) {
32
if (
33
- !props[propName] ||
33
+ !(propName in props) ||
34
hasReadOnlyValue[props.type] ||
35
props.onChange ||
36
props.readOnly ||
37
- props.disabled
37
+ props.disabled ||
38
+ props[propName] == null
39
) {
40
return null;
41
}
@@ -47,10 +48,11 @@ if (__DEV__) {
48
},
49
checked: function(props, propName, componentName) {
50
if (
50
- !props[propName] ||
51
+ !(propName in props) ||
52
props.onChange ||
53
props.readOnly ||
53
- props.disabled
54
+ props.disabled ||
55
+ props[propName] == null
56
) {
57
return null;
58
}