Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 20 additions & 22 deletions src/browser/ui/dom/components/ReactDOMInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactDOM = require('ReactDOM');
var ReactMount = require('ReactMount');
var ReactUpdates = require('ReactUpdates');

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

var instancesByReactID = {};

function forceUpdateIfMounted() {
/*jshint validthis:true */
if (this.isMounted()) {
this.forceUpdate();
}
}

/**
* Implements an <input> native component that allows setting these optional
* props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
Expand All @@ -58,16 +66,11 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
getInitialState: function() {
var defaultValue = this.props.defaultValue;
return {
checked: this.props.defaultChecked || false,
value: defaultValue != null ? defaultValue : null
initialChecked: this.props.defaultChecked || false,
initialValue: defaultValue != null ? defaultValue : null
};
},

shouldComponentUpdate: function() {
// Defer any updates to this component during the `onChange` handler.
return !this._isChanging;
},

render: function() {
// Clone `this.props` so we don't mutate the input.
var props = merge(this.props);
Expand All @@ -76,10 +79,10 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
props.defaultValue = null;

var value = LinkedValueUtils.getValue(this);
props.value = value != null ? value : this.state.value;
props.value = value != null ? value : this.state.initialValue;

var checked = LinkedValueUtils.getChecked(this);
props.checked = checked != null ? checked : this.state.checked;
props.checked = checked != null ? checked : this.state.initialChecked;

props.onChange = this._handleChange;

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

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

Expand Down
23 changes: 15 additions & 8 deletions src/browser/ui/dom/components/ReactDOMSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,21 @@ var LinkedValueUtils = require('LinkedValueUtils');
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactDOM = require('ReactDOM');
var ReactUpdates = require('ReactUpdates');

var merge = require('merge');

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

function updateWithPendingValueIfMounted() {
/*jshint validthis:true */
if (this.isMounted()) {
this.setState({value: this._pendingValue});
this._pendingValue = 0;
}
}

/**
* Validation function for `value` and `defaultValue`.
* @private
Expand Down Expand Up @@ -114,6 +123,10 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({
return {value: this.props.defaultValue || (this.props.multiple ? [] : '')};
},

componentWillMount: function() {
this._pendingValue = null;
},

componentWillReceiveProps: function(nextProps) {
if (!this.props.multiple && nextProps.multiple) {
this.setState({value: [this.state.value]});
Expand All @@ -122,11 +135,6 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({
}
},

shouldComponentUpdate: function() {
// Defer any updates to this component during the `onChange` handler.
return !this._isChanging;
},

render: function() {
// Clone `this.props` so we don't mutate the input.
var props = merge(this.props);
Expand Down Expand Up @@ -154,9 +162,7 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({
var returnValue;
var onChange = LinkedValueUtils.getOnChange(this);
if (onChange) {
this._isChanging = true;
returnValue = onChange.call(this, event);
this._isChanging = false;
}

var selectedValue;
Expand All @@ -172,7 +178,8 @@ var ReactDOMSelect = ReactCompositeComponent.createClass({
selectedValue = event.target.value;
}

this.setState({value: selectedValue});
this._pendingValue = selectedValue;
ReactUpdates.setImmediate(updateWithPendingValueIfMounted, this);
return returnValue;
}

Expand Down
17 changes: 9 additions & 8 deletions src/browser/ui/dom/components/ReactDOMTextarea.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var LinkedValueUtils = require('LinkedValueUtils');
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactDOM = require('ReactDOM');
var ReactUpdates = require('ReactUpdates');

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

function forceUpdateIfMounted() {
/*jshint validthis:true */
if (this.isMounted()) {
this.forceUpdate();
}
}

/**
* Implements a <textarea> native component that allows setting `value`, and
* `defaultValue`. This differs from the traditional DOM API because value is
Expand Down Expand Up @@ -92,11 +100,6 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({
};
},

shouldComponentUpdate: function() {
// Defer any updates to this component during the `onChange` handler.
return !this._isChanging;
},

render: function() {
// Clone `this.props` so we don't mutate the input.
var props = merge(this.props);
Expand Down Expand Up @@ -129,11 +132,9 @@ var ReactDOMTextarea = ReactCompositeComponent.createClass({
var returnValue;
var onChange = LinkedValueUtils.getOnChange(this);
if (onChange) {
this._isChanging = true;
returnValue = onChange.call(this, event);
this._isChanging = false;
}
this.setState({value: event.target.value});
ReactUpdates.setImmediate(forceUpdateIfMounted, this);
return returnValue;
}

Expand Down
41 changes: 34 additions & 7 deletions src/core/ReactUpdates.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ var mixInto = require('mixInto');
var warning = require('warning');

var dirtyComponents = [];
var setImmediateCallbackQueue = CallbackQueue.getPooled();
var setImmediateEnqueued = false;

var batchingStrategy = null;

Expand Down Expand Up @@ -73,7 +75,7 @@ var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];
function ReactUpdatesFlushTransaction() {
this.reinitializeTransaction();
this.dirtyComponentsLength = null;
this.callbackQueue = CallbackQueue.getPooled(null);
this.callbackQueue = CallbackQueue.getPooled();
this.reconcileTransaction =
ReactUpdates.ReactReconcileTransaction.getPooled();
}
Expand Down Expand Up @@ -170,11 +172,21 @@ var flushBatchedUpdates = ReactPerf.measure(
// ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
// array and perform any updates enqueued by mount-ready handlers (i.e.,
// componentDidUpdate) but we need to check here too in order to catch
// updates enqueued by setState callbacks.
while (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
// updates enqueued by setState callbacks and setImmediate calls.
while (dirtyComponents.length || setImmediateEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}

if (setImmediateEnqueued) {
setImmediateEnqueued = false;
var queue = setImmediateCallbackQueue;
setImmediateCallbackQueue = CallbackQueue.getPooled();
queue.notifyAll();
CallbackQueue.release(queue);
}
}
}
);
Expand Down Expand Up @@ -221,6 +233,20 @@ function enqueueUpdate(component, callback) {
}
}

/**
* Enqueue a callback to be run at the end of the current batching cycle. Throws
* if no updates are currently being performed.
*/
function setImmediate(callback, context) {
invariant(
batchingStrategy.isBatchingUpdates,
'ReactUpdates.setImmediate: Can\'t enqueue an immediate callback in a ' +
'context where updates are not being batched.'
);
setImmediateCallbackQueue.enqueue(callback, context);
setImmediateEnqueued = true;
}

var ReactUpdatesInjection = {
injectReconcileTransaction: function(ReconcileTransaction) {
invariant(
Expand Down Expand Up @@ -259,7 +285,8 @@ var ReactUpdates = {
batchedUpdates: batchedUpdates,
enqueueUpdate: enqueueUpdate,
flushBatchedUpdates: flushBatchedUpdates,
injection: ReactUpdatesInjection
injection: ReactUpdatesInjection,
setImmediate: setImmediate
};

module.exports = ReactUpdates;
75 changes: 75 additions & 0 deletions src/core/__tests__/ReactUpdates-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -748,4 +748,79 @@ describe('ReactUpdates', function() {
React.renderComponent(<A x={2} />, container);
expect(callbackCount).toBe(1);
});

it('calls setImmediate callbacks properly', function() {
var callbackCount = 0;
var A = React.createClass({
render: function() {
return <div />;
},
componentDidUpdate: function() {
var component = this;
ReactUpdates.setImmediate(function() {
expect(this).toBe(component);
callbackCount++;
ReactUpdates.setImmediate(function() {
callbackCount++;
});
expect(callbackCount).toBe(1);
}, this);
expect(callbackCount).toBe(0);
}
});

var container = document.createElement('div');
var component = React.renderComponent(<A />, container);
component.forceUpdate();
expect(callbackCount).toBe(2);
});

it('calls setImmediate callbacks with queued updates', function() {
var log = [];
var A = React.createClass({
getInitialState: () => ({updates: 0}),
render: function() {
log.push('render-' + this.state.updates);
return <div />;
},
componentDidUpdate: function() {
if (this.state.updates === 1) {
ReactUpdates.setImmediate(function() {
this.setState({updates: 2}, function() {
ReactUpdates.setImmediate(function() {
log.push('setImmediate-1.2');
});
log.push('setState-cb');
});
log.push('setImmediate-1.1');
}, this);
} else if (this.state.updates === 2) {
ReactUpdates.setImmediate(function() {
log.push('setImmediate-2');
});
}
log.push('didUpdate-' + this.state.updates);
}
});

var container = document.createElement('div');
var component = React.renderComponent(<A />, container);
component.setState({updates: 1});
expect(log).toEqual([
'render-0',
// We do the first update...
'render-1',
'didUpdate-1',
// ...which calls a setImmediate and enqueues a second update...
'setImmediate-1.1',
// ...which runs and enqueues the setImmediate-2 log in its didUpdate...
'render-2',
'didUpdate-2',
// ...and runs the setState callback, which enqueues the log for
// setImmediate-1.2.
'setState-cb',
'setImmediate-2',
'setImmediate-1.2'
]);
});
});