Files
react/src/core/ReactUpdateQueue.js
T
Lee Byron 279b956c9b Set state takes a function
This diff enables setState to accept a function in addition to a state partial. If you provide a function, it will be called with the up-to-date `state, props, context` as arguments.

This enables some nicer syntax for complex setState patterns:

If setState is doing an increment and wants to guarantee atomicy, you need a function:

```
this.setState(state => ({ number: state.number + 1 }));
```

This atomicy is particularly important if setState is called multiple times in a single frame of execution as the result of complex user actions. It's a tricky bug to chase down and difficult to determine how to fix when you find it. The current pattern of reaching into _pendingState relies on an implementation detail.

In this example: props.doAction() may result in your ancestor re-rendering and providing you with new props. If setState is called directly with an object literal referencing `this.props`, it will use the *old* version of props, not the new value. Using a function solves for this case:

```
this.props.doAction();
this.setState((state, props) => ({ number: state.number * props.multiplier }));
```
2015-02-03 19:49:17 -05:00

264 lines
8.5 KiB
JavaScript

/**
* Copyright 2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactUpdateQueue
*/
"use strict";
var ReactLifeCycle = require('ReactLifeCycle');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactElement = require('ReactElement');
var ReactInstanceMap = require('ReactInstanceMap');
var ReactUpdates = require('ReactUpdates');
var assign = require('Object.assign');
var invariant = require('invariant');
function enqueueUpdate(internalInstance) {
if (internalInstance !== ReactLifeCycle.currentlyMountingInstance) {
// If we're in a componentWillMount handler, don't enqueue a rerender
// because ReactUpdates assumes we're in a browser context (which is
// wrong for server rendering) and we're about to do a render anyway.
// See bug in #1740.
ReactUpdates.enqueueUpdate(internalInstance);
}
}
function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
invariant(
ReactCurrentOwner.current == null,
'%s(...): Cannot update during an existing state transition ' +
'(such as within `render`). Render methods should be a pure function ' +
'of props and state.',
callerName
);
var internalInstance = ReactInstanceMap.get(publicInstance);
invariant(
internalInstance,
'%s(...): Can only update a mounted or mounting component. ' +
'This usually means you called %s() on an unmounted ' +
'component.',
callerName,
callerName
);
invariant(
internalInstance !== ReactLifeCycle.currentlyUnmountingInstance,
'%s(...): Cannot call %s() on an unmounting component.',
callerName,
callerName
);
return internalInstance;
}
/**
* ReactUpdateQueue allows for state updates to be scheduled into a later
* reconciliation step.
*/
var ReactUpdateQueue = {
/**
* Enqueue a callback that will be executed after all the pending updates
* have processed.
*
* @param {ReactClass} publicInstance The instance to use as `this` context.
* @param {?function} callback Called after state is updated.
* @internal
*/
enqueueCallback: function(publicInstance, callback) {
invariant(
typeof callback === 'function',
'enqueueCallback(...): You called `setProps`, `replaceProps`, ' +
'`setState`, `replaceState`, or `forceUpdate` with a callback that ' +
'isn\'t callable.'
);
var internalInstance = ReactInstanceMap.get(publicInstance);
invariant(
internalInstance,
'Cannot enqueue a callback on an instance that is unmounted.'
);
if (internalInstance === ReactLifeCycle.currentlyMountingInstance) {
// Ignore callbacks in componentWillMount. See enqueueUpdate.
return;
}
if (internalInstance._pendingCallbacks) {
internalInstance._pendingCallbacks.push(callback);
} else {
internalInstance._pendingCallbacks = [callback];
}
// TODO: The callback here is ignored when setState is called from
// componentWillMount. Either fix it or disallow doing so completely in
// favor of getInitialState. Alternatively, we can disallow
// componentWillMount during server-side rendering.
enqueueUpdate(internalInstance);
},
enqueueCallbackInternal: function(internalInstance, callback) {
invariant(
typeof callback === "function",
'enqueueCallback(...): You called `setProps`, `replaceProps`, ' +
'`setState`, `replaceState`, or `forceUpdate` with a callback that ' +
'isn\'t callable.'
);
if (internalInstance._pendingCallbacks) {
internalInstance._pendingCallbacks.push(callback);
} else {
internalInstance._pendingCallbacks = [callback];
}
},
/**
* Forces an update. This should only be invoked when it is known with
* certainty that we are **not** in a DOM transaction.
*
* You may want to call this when you know that some deeper aspect of the
* component's state has changed but `setState` was not called.
*
* This will not invoke `shouldUpdateComponent`, but it will invoke
* `componentWillUpdate` and `componentDidUpdate`.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @internal
*/
enqueueForceUpdate: function(publicInstance) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'forceUpdate'
);
internalInstance._pendingForceUpdate = true;
enqueueUpdate(internalInstance);
},
/**
* Replaces all of the state. Always use this or `setState` 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.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @param {object} completeState Next state.
* @internal
*/
enqueueReplaceState: function(publicInstance, completeState) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'replaceState'
);
internalInstance._pendingStateQueue = [completeState];
internalInstance._pendingReplaceState = true;
enqueueUpdate(internalInstance);
},
/**
* Sets a subset of the state. This only exists because _pendingState is
* internal. This provides a merging strategy that is not available to deep
* properties which is confusing. TODO: Expose pendingState or don't use it
* during the merge.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @param {object} partialState Next partial state to be merged with state.
* @internal
*/
enqueueSetState: function(publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState'
);
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
},
/**
* Sets a subset of the props.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @param {object} partialProps Subset of the next props.
* @internal
*/
enqueueSetProps: function(publicInstance, partialProps) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setProps'
);
invariant(
internalInstance._isTopLevel,
'setProps(...): You called `setProps` on a ' +
'component with a parent. This is an anti-pattern since props will ' +
'get reactively updated when rendered. Instead, change the owner\'s ' +
'`render` method to pass the correct value as props to the component ' +
'where it is created.'
);
// Merge with the pending element if it exists, otherwise with existing
// element props.
var element = internalInstance._pendingElement ||
internalInstance._currentElement;
var props = assign({}, element.props, partialProps);
internalInstance._pendingElement = ReactElement.cloneAndReplaceProps(
element,
props
);
enqueueUpdate(internalInstance);
},
/**
* Replaces all of the props.
*
* @param {ReactClass} publicInstance The instance that should rerender.
* @param {object} props New props.
* @internal
*/
enqueueReplaceProps: function(publicInstance, props) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'replaceProps'
);
invariant(
internalInstance._isTopLevel,
'replaceProps(...): You called `replaceProps` on a ' +
'component with a parent. This is an anti-pattern since props will ' +
'get reactively updated when rendered. Instead, change the owner\'s ' +
'`render` method to pass the correct value as props to the component ' +
'where it is created.'
);
// Merge with the pending element if it exists, otherwise with existing
// element props.
var element = internalInstance._pendingElement ||
internalInstance._currentElement;
internalInstance._pendingElement = ReactElement.cloneAndReplaceProps(
element,
props
);
enqueueUpdate(internalInstance);
},
enqueueElementInternal: function(internalInstance, newElement) {
internalInstance._pendingElement = newElement;
enqueueUpdate(internalInstance);
}
};
module.exports = ReactUpdateQueue;