@@ -1043,6 +1043,63 @@ describe('ReactDOMComponent', () => {
10431043 expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 2 ) ;
10441044 } ) ;
10451045
1046+ it ( 'should not incur unnecessary DOM mutations for controlled string properties' , ( ) => {
1047+ function onChange ( ) { }
1048+ const container = document . createElement ( 'div' ) ;
1049+ ReactDOM . render ( < input value = "" onChange = { onChange } /> , container ) ;
1050+
1051+ const node = container . firstChild ;
1052+
1053+ let nodeValue = '' ;
1054+ const nodeValueSetter = jest . fn ( ) ;
1055+ Object . defineProperty ( node , 'value' , {
1056+ get : function ( ) {
1057+ return nodeValue ;
1058+ } ,
1059+ set : nodeValueSetter . mockImplementation ( function ( newValue ) {
1060+ nodeValue = newValue ;
1061+ } ) ,
1062+ } ) ;
1063+
1064+ ReactDOM . render ( < input value = "foo" onChange = { onChange } /> , container ) ;
1065+ expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 1 ) ;
1066+
1067+ ReactDOM . render (
1068+ < input value = "foo" data-unrelated = { true } onChange = { onChange } /> ,
1069+ container ,
1070+ ) ;
1071+ expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 1 ) ;
1072+
1073+ expect ( ( ) => {
1074+ ReactDOM . render ( < input onChange = { onChange } /> , container ) ;
1075+ } ) . toErrorDev (
1076+ 'A component is changing a controlled input to be uncontrolled. This is likely caused by ' +
1077+ 'the value changing from a defined to undefined, which should not happen. Decide between ' +
1078+ 'using a controlled or uncontrolled input element for the lifetime of the component.' ,
1079+ ) ;
1080+ expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 1 ) ;
1081+
1082+ expect ( ( ) => {
1083+ ReactDOM . render ( < input value = { null } onChange = { onChange } /> , container ) ;
1084+ } ) . toErrorDev (
1085+ 'value` prop on `input` should not be null. Consider using an empty string to clear the ' +
1086+ 'component or `undefined` for uncontrolled components.' ,
1087+ ) ;
1088+ expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 1 ) ;
1089+
1090+ expect ( ( ) => {
1091+ ReactDOM . render ( < input value = "" onChange = { onChange } /> , container ) ;
1092+ } ) . toErrorDev (
1093+ ' A component is changing an uncontrolled input to be controlled. This is likely caused by ' +
1094+ 'the value changing from undefined to a defined value, which should not happen. Decide between ' +
1095+ 'using a controlled or uncontrolled input element for the lifetime of the component.' ,
1096+ ) ;
1097+ expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 2 ) ;
1098+
1099+ ReactDOM . render ( < input onChange = { onChange } /> , container ) ;
1100+ expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 2 ) ;
1101+ } ) ;
1102+
10461103 it ( 'should not incur unnecessary DOM mutations for boolean properties' , ( ) => {
10471104 const container = document . createElement ( 'div' ) ;
10481105 function onChange ( ) {
@@ -1066,7 +1123,12 @@ describe('ReactDOMComponent', () => {
10661123 } ) ;
10671124
10681125 ReactDOM . render (
1069- < input type = "checkbox" onChange = { onChange } checked = { true } /> ,
1126+ < input
1127+ type = "checkbox"
1128+ onChange = { onChange }
1129+ checked = { true }
1130+ data-unrelated = { true }
1131+ /> ,
10701132 container ,
10711133 ) ;
10721134 expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 0 ) ;
@@ -1094,15 +1156,13 @@ describe('ReactDOMComponent', () => {
10941156 'using a controlled or uncontrolled input element for the lifetime of the component.' ,
10951157 ) ;
10961158
1097- // TODO: Non-null values are updated twice on inputs. This is should ideally be fixed.
1098- expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 3 ) ;
1159+ expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 2 ) ;
10991160
11001161 ReactDOM . render (
11011162 < input type = "checkbox" onChange = { onChange } checked = { true } /> ,
11021163 container ,
11031164 ) ;
1104- // TODO: Non-null values are updated twice on inputs. This is should ideally be fixed.
1105- expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 5 ) ;
1165+ expect ( nodeValueSetter ) . toHaveBeenCalledTimes ( 3 ) ;
11061166 } ) ;
11071167
11081168 it ( 'should ignore attribute list for elements with the "is" attribute' , ( ) => {
0 commit comments