Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch together calls to setState, setProps, etc #115

Merged
merged 6 commits into from
Jul 3, 2013
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,17 @@ Transfer properties from this component to a target component that have not alre
#### setState

```javascript
setState(object nextState)
setState(object nextState[, function callback])
```

Merges nextState with the current state. This is the primary method you use to trigger UI updates from event handlers and server request callbacks.
Merges nextState with the current state. This is the primary method you use to trigger UI updates from event handlers and server request callbacks. In addition, you can supply an optional callback function that is executed once `setState` is completed.

**Note:** *NEVER* mutate `this.state` directly. As calling `setState()` afterwards may replace the mutation you made. Treat `this.state` as if it were immutable.

**Note:** `setState()` does not immediately mutate `this.state` but creates a pending state transition. Accessing `this.state` after calling this method can potentially return the existing value.

**Note**: There is no guarantee of synchronous operation of calls to `setState` and calls may eventually be batched for performance gains.

#### replaceState

```javascript
Expand Down
73 changes: 62 additions & 11 deletions src/core/ReactComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var ReactID = require('ReactID');
var ReactMount = require('ReactMount');
var ReactOwner = require('ReactOwner');
var ReactReconcileTransaction = require('ReactReconcileTransaction');
var ReactUpdates = require('ReactUpdates');

var invariant = require('invariant');
var keyMirror = require('keyMirror');
Expand Down Expand Up @@ -261,21 +262,27 @@ var ReactComponent = {
* Sets a subset of the props.
*
* @param {object} partialProps Subset of the next props.
* @param {?function} callback Called after props are updated.
* @final
* @public
*/
setProps: function(partialProps) {
this.replaceProps(merge(this.props, partialProps));
setProps: function(partialProps, callback) {
// Merge with `_pendingProps` if it exists, otherwise with existing props.
this.replaceProps(
merge(this._pendingProps || this.props, partialProps),
callback
);
},

/**
* Replaces all of the props.
*
* @param {object} props New props.
* @param {?function} callback Called after props are updated.
* @final
* @public
*/
replaceProps: function(props) {
replaceProps: function(props, callback) {
invariant(
!this.props[OWNER],
'replaceProps(...): You called `setProps` or `replaceProps` on a ' +
Expand All @@ -284,9 +291,8 @@ var ReactComponent = {
'`render` method to pass the correct value as props to the component ' +
'where it is created.'
);
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
transaction.perform(this.receiveProps, this, props, transaction);
ReactComponent.ReactReconcileTransaction.release(transaction);
this._pendingProps = props;
ReactUpdates.enqueueUpdate(this, callback);
},

/**
Expand All @@ -306,6 +312,9 @@ var ReactComponent = {
// All components start unmounted.
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;

this._pendingProps = null;
this._pendingCallbacks = null;

// Children can be more than one argument
var childrenLength = arguments.length - 1;
if (childrenLength === 1) {
Expand Down Expand Up @@ -392,17 +401,59 @@ var ReactComponent = {
this.isMounted(),
'receiveProps(...): Can only update a mounted component.'
);
this._pendingProps = nextProps;
this._performUpdateIfNecessary(transaction);
},

/**
* Call `_performUpdateIfNecessary` within a new transaction.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
performUpdateIfNecessary: function() {
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
transaction.perform(this._performUpdateIfNecessary, this, transaction);
ReactComponent.ReactReconcileTransaction.release(transaction);
},

/**
* If `_pendingProps` is set, update the component.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_performUpdateIfNecessary: function(transaction) {
if (this._pendingProps == null) {
return;
}
var prevProps = this.props;
this.props = this._pendingProps;
this._pendingProps = null;
this.updateComponent(transaction, prevProps);
},

/**
* Updates the component's currently mounted representation.
*
* @param {ReactReconcileTransaction} transaction
* @param {object} prevProps
* @internal
*/
updateComponent: function(transaction, prevProps) {
var props = this.props;
// If either the owner or a `ref` has changed, make sure the newest owner
// has stored a reference to `this`, and the previous owner (if different)
// has forgotten the reference to `this`.
if (nextProps[OWNER] !== props[OWNER] || nextProps.ref !== props.ref) {
if (props.ref != null) {
ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]);
if (props[OWNER] !== prevProps[OWNER] || props.ref !== prevProps.ref) {
if (prevProps.ref != null) {
ReactOwner.removeComponentAsRefFrom(
this, prevProps.ref, prevProps[OWNER]
);
}
// Correct, even if the owner is the same, and only the ref has changed.
if (nextProps.ref != null) {
ReactOwner.addComponentAsRefTo(this, nextProps.ref, nextProps[OWNER]);
if (props.ref != null) {
ReactOwner.addComponentAsRefTo(this, props.ref, props[OWNER]);
}
}
},
Expand Down
137 changes: 68 additions & 69 deletions src/core/ReactCompositeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var ReactComponent = require('ReactComponent');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactOwner = require('ReactOwner');
var ReactPropTransferer = require('ReactPropTransferer');
var ReactUpdates = require('ReactUpdates');

var invariant = require('invariant');
var keyMirror = require('keyMirror');
Expand Down Expand Up @@ -507,6 +508,7 @@ var ReactCompositeComponentMixin = {

this.state = this.getInitialState ? this.getInitialState() : null;
this._pendingState = null;
this._pendingForceUpdate = false;

if (this.componentWillMount) {
this.componentWillMount();
Expand Down Expand Up @@ -558,45 +560,29 @@ var ReactCompositeComponentMixin = {
// TODO: this.state = null;
},

/**
* Updates the rendered DOM nodes given a new set of props.
*
* @param {object} nextProps Next set of properties.
* @param {ReactReconcileTransaction} transaction
* @final
* @internal
*/
receiveProps: function(nextProps, transaction) {
this._processProps(nextProps);
ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);

this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
if (this.componentWillReceiveProps) {
this.componentWillReceiveProps(nextProps, transaction);
}
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
// When receiving props, calls to `setState` by `componentWillReceiveProps`
// will set `this._pendingState` without triggering a re-render.
var nextState = this._pendingState || this.state;
this._pendingState = null;
this._receivePropsAndState(nextProps, nextState, transaction);
this._compositeLifeCycleState = null;
},

/**
* Sets a subset of the state. Always use this or `replaceState` to mutate
* state. You should treat `this.state` as immutable.
*
* There is no guarantee that `this.state` will be immediately updated, so
* accessing `this.state` after calling this method may return the old value.
*
* There is no guarantee that calls to `setState` will run synchronously,
* as they may eventually be batched together. You can provide an optional
* callback that will be executed when the call to setState is actually
* completed.
*
* @param {object} partialState Next partial state to be merged with state.
* @param {?function} callback Called after state is updated.
* @final
* @protected
*/
setState: function(partialState) {
setState: function(partialState, callback) {
// Merge with `_pendingState` if it exists, otherwise with existing state.
this.replaceState(merge(this._pendingState || this.state, partialState));
this.replaceState(
merge(this._pendingState || this.state, partialState),
callback
);
},

/**
Expand All @@ -607,34 +593,14 @@ var ReactCompositeComponentMixin = {
* accessing `this.state` after calling this method may return the old value.
*
* @param {object} completeState Next state.
* @param {?function} callback Called after state is updated.
* @final
* @protected
*/
replaceState: function(completeState) {
var compositeLifeCycleState = this._compositeLifeCycleState;
replaceState: function(completeState, callback) {
validateLifeCycleOnReplaceState.call(null, this);
this._pendingState = completeState;

// Do not trigger a state transition if we are in the middle of mounting or
// receiving props because both of those will already be doing this.
if (compositeLifeCycleState !== CompositeLifeCycle.MOUNTING &&
compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_PROPS) {
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;

var nextState = this._pendingState;
this._pendingState = null;

var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
transaction.perform(
this._receivePropsAndState,
this,
this.props,
nextState,
transaction
);
ReactComponent.ReactReconcileTransaction.release(transaction);
this._compositeLifeCycleState = null;
}
ReactUpdates.enqueueUpdate(this, callback);
},

/**
Expand Down Expand Up @@ -664,18 +630,52 @@ var ReactCompositeComponentMixin = {
}
},

performUpdateIfNecessary: function() {
var compositeLifeCycleState = this._compositeLifeCycleState;
// Do not trigger a state transition if we are in the middle of mounting or
// receiving props because both of those will already be doing this.
if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING ||
compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
return;
}
ReactComponent.Mixin.performUpdateIfNecessary.call(this);
},

/**
* Receives next props and next state, and negotiates whether or not the
* component should update as a result.
* If any of `_pendingProps`, `_pendingState`, or `_pendingForceUpdate` is
* set, update the component.
*
* @param {object} nextProps Next object to set as props.
* @param {?object} nextState Next object to set as state.
* @param {ReactReconcileTransaction} transaction
* @private
* @internal
*/
_receivePropsAndState: function(nextProps, nextState, transaction) {
if (!this.shouldComponentUpdate ||
_performUpdateIfNecessary: function(transaction) {
if (this._pendingProps == null &&
this._pendingState == null &&
!this._pendingForceUpdate) {
return;
}

var nextProps = this.props;
if (this._pendingProps != null) {
nextProps = this._pendingProps;
this._processProps(nextProps);
this._pendingProps = null;

this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
if (this.componentWillReceiveProps) {
this.componentWillReceiveProps(nextProps, transaction);
}
}

this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;

var nextState = this._pendingState || this.state;
this._pendingState = null;

if (this._pendingForceUpdate ||
!this.shouldComponentUpdate ||
this.shouldComponentUpdate(nextProps, nextState)) {
this._pendingForceUpdate = false;
// Will set `this.props` and `this.state`.
this._performComponentUpdate(nextProps, nextState, transaction);
} else {
Expand All @@ -684,6 +684,8 @@ var ReactCompositeComponentMixin = {
this.props = nextProps;
this.state = nextState;
}

this._compositeLifeCycleState = null;
},

/**
Expand All @@ -706,7 +708,7 @@ var ReactCompositeComponentMixin = {
this.props = nextProps;
this.state = nextState;

this.updateComponent(transaction);
this.updateComponent(transaction, prevProps, prevState);

if (this.componentDidUpdate) {
transaction.getReactOnDOMReady().enqueue(
Expand All @@ -723,10 +725,13 @@ var ReactCompositeComponentMixin = {
* Sophisticated clients may wish to override this.
*
* @param {ReactReconcileTransaction} transaction
* @param {object} prevProps
* @param {?object} prevState
* @internal
* @overridable
*/
updateComponent: function(transaction) {
updateComponent: function(transaction, prevProps, prevState) {
ReactComponent.Mixin.updateComponent.call(this, transaction, prevProps);
var currentComponent = this._renderedComponent;
var nextComponent = this._renderValidatedComponent();
if (currentComponent.constructor === nextComponent.constructor) {
Expand Down Expand Up @@ -755,10 +760,11 @@ var ReactCompositeComponentMixin = {
* This will not invoke `shouldUpdateComponent`, but it will invoke
* `componentWillUpdate` and `componentDidUpdate`.
*
* @param {?function} callback Called after update is complete.
* @final
* @protected
*/
forceUpdate: function() {
forceUpdate: function(callback) {
var compositeLifeCycleState = this._compositeLifeCycleState;
invariant(
this.isMounted() ||
Expand All @@ -772,15 +778,8 @@ var ReactCompositeComponentMixin = {
'forceUpdate(...): Cannot force an update while unmounting component ' +
'or during an existing state transition (such as within `render`).'
);
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
transaction.perform(
this._performComponentUpdate,
this,
this.props,
this.state,
transaction
);
ReactComponent.ReactReconcileTransaction.release(transaction);
this._pendingForceUpdate = true;
ReactUpdates.enqueueUpdate(this, callback);
},

/**
Expand Down