mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Fix bug with double updates in a single batch (#6650)
Fixes #2410. Fixes #6371. Fixes #6538. I also manually tested the codepen in #3762 and verified it now works.
This commit is contained in:
@@ -153,6 +153,7 @@ var ReactCompositeComponentMixin = {
|
||||
this._nativeContainerInfo = null;
|
||||
|
||||
// See ReactUpdateQueue
|
||||
this._updateBatchNumber = null;
|
||||
this._pendingElement = null;
|
||||
this._pendingStateQueue = null;
|
||||
this._pendingReplaceState = false;
|
||||
@@ -765,9 +766,7 @@ var ReactCompositeComponentMixin = {
|
||||
transaction,
|
||||
this._context
|
||||
);
|
||||
}
|
||||
|
||||
if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
|
||||
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
|
||||
this.updateComponent(
|
||||
transaction,
|
||||
this._currentElement,
|
||||
@@ -775,6 +774,8 @@ var ReactCompositeComponentMixin = {
|
||||
this._context,
|
||||
this._context
|
||||
);
|
||||
} else {
|
||||
this._updateBatchNumber = null;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -878,6 +879,7 @@ var ReactCompositeComponentMixin = {
|
||||
);
|
||||
}
|
||||
|
||||
this._updateBatchNumber = null;
|
||||
if (shouldUpdate) {
|
||||
this._pendingForceUpdate = false;
|
||||
// Will set `this.props`, `this.state` and `this.context`.
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
var ReactRef = require('ReactRef');
|
||||
var ReactInstrumentation = require('ReactInstrumentation');
|
||||
|
||||
var invariant = require('invariant');
|
||||
|
||||
/**
|
||||
* Helper to call ReactRef.attachRefs with this composite component, split out
|
||||
* to avoid allocations in the transaction mount-ready queue.
|
||||
@@ -190,8 +192,22 @@ var ReactReconciler = {
|
||||
*/
|
||||
performUpdateIfNecessary: function(
|
||||
internalInstance,
|
||||
transaction
|
||||
transaction,
|
||||
updateBatchNumber
|
||||
) {
|
||||
if (internalInstance._updateBatchNumber !== updateBatchNumber) {
|
||||
// The component's enqueued batch number should always be the current
|
||||
// batch or the following one.
|
||||
invariant(
|
||||
internalInstance._updateBatchNumber == null ||
|
||||
internalInstance._updateBatchNumber === updateBatchNumber + 1,
|
||||
'performUpdateIfNecessary: Unexpected batch number (current %s, ' +
|
||||
'pending %s)',
|
||||
updateBatchNumber,
|
||||
internalInstance._updateBatchNumber
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (__DEV__) {
|
||||
if (internalInstance._debugID !== 0) {
|
||||
ReactInstrumentation.debugTool.onBeginReconcilerTimer(
|
||||
|
||||
@@ -22,6 +22,7 @@ var Transaction = require('Transaction');
|
||||
var invariant = require('invariant');
|
||||
|
||||
var dirtyComponents = [];
|
||||
var updateBatchNumber = 0;
|
||||
var asapCallbackQueue = CallbackQueue.getPooled();
|
||||
var asapEnqueued = false;
|
||||
|
||||
@@ -138,6 +139,13 @@ function runBatchedUpdates(transaction) {
|
||||
// them before their children by sorting the array.
|
||||
dirtyComponents.sort(mountOrderComparator);
|
||||
|
||||
// Any updates enqueued while reconciling must be performed after this entire
|
||||
// batch. Otherwise, if dirtyComponents is [A, B] where A has children B and
|
||||
// C, B could update twice in a single batch if C's render enqueues an update
|
||||
// to B (since B would have already updated, we should skip it, and the only
|
||||
// way we can know to do so is by checking the batch counter).
|
||||
updateBatchNumber++;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
// If a component is unmounted before pending changes apply, it will still
|
||||
// be here, but we assume that it has cleared its _pendingCallbacks and
|
||||
@@ -166,7 +174,8 @@ function runBatchedUpdates(transaction) {
|
||||
|
||||
ReactReconciler.performUpdateIfNecessary(
|
||||
component,
|
||||
transaction.reconcileTransaction
|
||||
transaction.reconcileTransaction,
|
||||
updateBatchNumber
|
||||
);
|
||||
|
||||
if (markerName) {
|
||||
@@ -238,6 +247,9 @@ function enqueueUpdate(component) {
|
||||
}
|
||||
|
||||
dirtyComponents.push(component);
|
||||
if (component._updateBatchNumber == null) {
|
||||
component._updateBatchNumber = updateBatchNumber + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1024,4 +1024,102 @@ describe('ReactUpdates', function() {
|
||||
'to be a function. Instead received: Foo (keys: a, b).'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not update one component twice in a batch (#2410)', function() {
|
||||
var Parent = React.createClass({
|
||||
getChild: function() {
|
||||
return this.refs.child;
|
||||
},
|
||||
render: function() {
|
||||
return <Child ref="child" />;
|
||||
},
|
||||
});
|
||||
|
||||
var renderCount = 0;
|
||||
var postRenderCount = 0;
|
||||
var once = false;
|
||||
var Child = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {updated: false};
|
||||
},
|
||||
componentWillUpdate: function() {
|
||||
if (!once) {
|
||||
once = true;
|
||||
this.setState({updated: true});
|
||||
}
|
||||
},
|
||||
componentDidMount: function() {
|
||||
expect(renderCount).toBe(postRenderCount + 1);
|
||||
postRenderCount++;
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
expect(renderCount).toBe(postRenderCount + 1);
|
||||
postRenderCount++;
|
||||
},
|
||||
render: function() {
|
||||
expect(renderCount).toBe(postRenderCount);
|
||||
renderCount++;
|
||||
return <div />;
|
||||
},
|
||||
});
|
||||
|
||||
var parent = ReactTestUtils.renderIntoDocument(<Parent />);
|
||||
var child = parent.getChild();
|
||||
ReactDOM.unstable_batchedUpdates(function() {
|
||||
parent.forceUpdate();
|
||||
child.forceUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update one component twice in a batch (#6371)', function() {
|
||||
var callbacks = [];
|
||||
function emitChange() {
|
||||
callbacks.forEach(c => c());
|
||||
}
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { showChild: true };
|
||||
}
|
||||
componentDidMount() {
|
||||
console.log('about to remove child via set state');
|
||||
this.setState({ showChild: false });
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ForceUpdatesOnChange />
|
||||
{this.state.showChild && <EmitsChangeOnUnmount />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmitsChangeOnUnmount extends React.Component {
|
||||
componentWillUnmount() {
|
||||
emitChange();
|
||||
}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class ForceUpdatesOnChange extends React.Component {
|
||||
componentDidMount() {
|
||||
this.onChange = () => this.forceUpdate();
|
||||
this.onChange();
|
||||
callbacks.push(this.onChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
callbacks = callbacks.filter((c) => c !== this.onChange);
|
||||
}
|
||||
render() {
|
||||
return <div key={Math.random()} onClick={function() {}} />;
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.createElement('div'));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user