mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
279b956c9b
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 }));
```
264 lines
8.5 KiB
JavaScript
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;
|