Skip to content

Commit 354fb44

Browse files
committed
Use setImmediate to defer value restoration
Depends on #1758. Fixes #1698. Previously, controlled components would update too soon when using something like ReactLayeredComponentMixin (i.e., before the layer's updates could propagate from the parent), causing the cursor to jump even when always updating the new model value to match the DOM state. With this change, we defer the update until after all nested updates have had a chance to finish, which prevents the cursor from misbehaving. Also cleaned up the logic around updating a bit -- the .value and .checked updates in ReactDOMInput weren't being relied on at all so I removed them and opted for a simple forceUpdate instead. I also got rid of _isChanging which hasn't been necessary since the introduction of update batching. Test Plan: Tested the example in http://jsfiddle.net/Bobris/ZZtXn/2/ and verified that the cursor didn't jump. Changed the code to filter out numbers and verified that the field prevents typing numbers (attempting to do so still causes the cursor to jump to the end). Also verified that controlled and uncontrolled radio buttons, textareas, and select boxes work.
1 parent 12b532c commit 354fb44

File tree

3 files changed

+44
-38
lines changed

3 files changed

+44
-38
lines changed

src/browser/ui/dom/components/ReactDOMInput.js

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
2525
var ReactCompositeComponent = require('ReactCompositeComponent');
2626
var ReactDOM = require('ReactDOM');
2727
var ReactMount = require('ReactMount');
28+
var ReactUpdates = require('ReactUpdates');
2829

2930
var invariant = require('invariant');
3031
var merge = require('merge');
@@ -34,6 +35,13 @@ var input = ReactDOM.input;
3435

3536
var instancesByReactID = {};
3637

38+
function forceUpdateIfMounted() {
39+
/*jshint validthis:true */
40+
if (this.isMounted()) {
41+
this.forceUpdate();
42+
}
43+
}
44+
3745
/**
3846
* Implements an <input> native component that allows setting these optional
3947
* props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
@@ -58,16 +66,11 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
5866
getInitialState: function() {
5967
var defaultValue = this.props.defaultValue;
6068
return {
61-
checked: this.props.defaultChecked || false,
62-
value: defaultValue != null ? defaultValue : null
69+
initialChecked: this.props.defaultChecked || false,
70+
initialValue: defaultValue != null ? defaultValue : null
6371
};
6472
},
6573

66-
shouldComponentUpdate: function() {
67-
// Defer any updates to this component during the `onChange` handler.
68-
return !this._isChanging;
69-
},
70-
7174
render: function() {
7275
// Clone `this.props` so we don't mutate the input.
7376
var props = merge(this.props);
@@ -76,10 +79,10 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
7679
props.defaultValue = null;
7780

7881
var value = LinkedValueUtils.getValue(this);
79-
props.value = value != null ? value : this.state.value;
82+
props.value = value != null ? value : this.state.initialValue;
8083

8184
var checked = LinkedValueUtils.getChecked(this);
82-
props.checked = checked != null ? checked : this.state.checked;
85+
props.checked = checked != null ? checked : this.state.initialChecked;
8386

8487
props.onChange = this._handleChange;
8588

@@ -119,14 +122,12 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
119122
var returnValue;
120123
var onChange = LinkedValueUtils.getOnChange(this);
121124
if (onChange) {
122-
this._isChanging = true;
123125
returnValue = onChange.call(this, event);
124-
this._isChanging = false;
125126
}
126-
this.setState({
127-
checked: event.target.checked,
128-
value: event.target.value
129-
});
127+
// Here we use setImmediate to wait until all updates have propagated, which
128+
// is important when using controlled components within layers:
129+
// https://github.com/facebook/react/issues/1698
130+
ReactUpdates.setImmediate(forceUpdateIfMounted, this);
130131

131132
var name = this.props.name;
132133
if (this.props.type === 'radio' && name != null) {
@@ -164,13 +165,10 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
164165
'ReactDOMInput: Unknown radio button ID %s.',
165166
otherID
166167
);
167-
// In some cases, this will actually change the `checked` state value.
168-
// In other cases, there's no change but this forces a reconcile upon
169-
// which componentDidUpdate will reset the DOM property to whatever it
170-
// should be.
171-
otherInstance.setState({
172-
checked: false
173-
});
168+
// If this is a controlled radio button group, forcing the input that
169+
// was previously checked to update will cause it to be come re-checked
170+
// as appropriate.
171+
ReactUpdates.setImmediate(forceUpdateIfMounted, otherInstance);
174172
}
175173
}
176174

src/browser/ui/dom/components/ReactDOMSelect.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,21 @@ var LinkedValueUtils = require('LinkedValueUtils');
2323
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
2424
var ReactCompositeComponent = require('ReactCompositeComponent');
2525
var ReactDOM = require('ReactDOM');
26+
var ReactUpdates = require('ReactUpdates');
2627

2728
var merge = require('merge');
2829

2930
// Store a reference to the <select> `ReactDOMComponent`.
3031
var select = ReactDOM.select;
3132

33+
function updateWithPendingValueIfMounted() {
34+
/*jshint validthis:true */
35+
if (this.isMounted()) {
36+
this.setState({value: this._pendingValue});
37+
this._pendingValue = 0;
38+
}
39+
}
40+
3241
/**
3342
* Validation function for `value` and `defaultValue`.
3443
* @private
@@ -114,6 +123,10 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({
114123
return {value: this.props.defaultValue || (this.props.multiple ? [] : '')};
115124
},
116125

126+
componentWillMount: function() {
127+
this._pendingValue = null;
128+
},
129+
117130
componentWillReceiveProps: function(nextProps) {
118131
if (!this.props.multiple && nextProps.multiple) {
119132
this.setState({value: [this.state.value]});
@@ -122,11 +135,6 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({
122135
}
123136
},
124137

125-
shouldComponentUpdate: function() {
126-
// Defer any updates to this component during the `onChange` handler.
127-
return !this._isChanging;
128-
},
129-
130138
render: function() {
131139
// Clone `this.props` so we don't mutate the input.
132140
var props = merge(this.props);
@@ -154,9 +162,7 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({
154162
var returnValue;
155163
var onChange = LinkedValueUtils.getOnChange(this);
156164
if (onChange) {
157-
this._isChanging = true;
158165
returnValue = onChange.call(this, event);
159-
this._isChanging = false;
160166
}
161167

162168
var selectedValue;
@@ -172,7 +178,8 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({
172178
selectedValue = event.target.value;
173179
}
174180

175-
this.setState({value: selectedValue});
181+
this._pendingValue = selectedValue;
182+
ReactUpdates.setImmediate(updateWithPendingValueIfMounted, this);
176183
return returnValue;
177184
}
178185

src/browser/ui/dom/components/ReactDOMTextarea.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ var LinkedValueUtils = require('LinkedValueUtils');
2424
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
2525
var ReactCompositeComponent = require('ReactCompositeComponent');
2626
var ReactDOM = require('ReactDOM');
27+
var ReactUpdates = require('ReactUpdates');
2728

2829
var invariant = require('invariant');
2930
var merge = require('merge');
@@ -33,6 +34,13 @@ var warning = require('warning');
3334
// Store a reference to the <textarea> `ReactDOMComponent`.
3435
var textarea = ReactDOM.textarea;
3536

37+
function forceUpdateIfMounted() {
38+
/*jshint validthis:true */
39+
if (this.isMounted()) {
40+
this.forceUpdate();
41+
}
42+
}
43+
3644
/**
3745
* Implements a <textarea> native component that allows setting `value`, and
3846
* `defaultValue`. This differs from the traditional DOM API because value is
@@ -92,11 +100,6 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({
92100
};
93101
},
94102

95-
shouldComponentUpdate: function() {
96-
// Defer any updates to this component during the `onChange` handler.
97-
return !this._isChanging;
98-
},
99-
100103
render: function() {
101104
// Clone `this.props` so we don't mutate the input.
102105
var props = merge(this.props);
@@ -129,11 +132,9 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({
129132
var returnValue;
130133
var onChange = LinkedValueUtils.getOnChange(this);
131134
if (onChange) {
132-
this._isChanging = true;
133135
returnValue = onChange.call(this, event);
134-
this._isChanging = false;
135136
}
136-
this.setState({value: event.target.value});
137+
ReactUpdates.setImmediate(forceUpdateIfMounted, this);
137138
return returnValue;
138139
}
139140

0 commit comments

Comments
 (0)