Don't mutate current tree before work is committed.

We should be able to abort an update without any side-effects to the
current tree. This fixes a few cases where that was broken.

The callback list should only ever be set on the workInProgress.
There's no reason to add it to the current tree because they're not
needed after they are called during the commit phase.

Also found a bug where the memoizedProps were set to null in the
case of an update, because the pendingProps were null. Fixed by
transfering the props from the instance, like we were already doing
with state.

Added a test to ensure that setState can be called inside a
callback.
This commit is contained in:
Andrew Clark
2016-09-13 15:26:48 -07:00
committed by Sebastian Markbage
parent f514662ca0
commit 0ca1cea26a
4 changed files with 49 additions and 10 deletions
@@ -32,12 +32,16 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
switch (finishedWork.tag) {
case ClassComponent: {
// Clear updates from current fiber. This must go before the callbacks
// are reset, in case an update is triggered from inside a callback. Is
// this safe? Relies on the assumption that work is only committed if
// the update queue is empty.
if (finishedWork.alternate) {
finishedWork.alternate.updateQueue = null;
}
if (finishedWork.callbackList) {
const { callbackList } = finishedWork;
finishedWork.callbackList = null;
if (finishedWork.alternate) {
finishedWork.alternate.callbackList = null;
}
callCallbacks(callbackList, finishedWork.stateNode);
}
// TODO: Fire componentDidMount/componentDidUpdate, update refs
@@ -132,14 +132,14 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
transferOutput(workInProgress.child, workInProgress);
// Don't use the state queue to compute the memoized state. We already
// merged it and assigned it to the instance. Transfer it from there.
const state = workInProgress.stateNode.state;
// Also need to transfer the props, because pendingProps will be null
// in the case of an update
const { state, props } = workInProgress.stateNode;
workInProgress.memoizedState = state;
workInProgress.memoizedProps = props;
// Transfer update queue to callbackList field so callbacks can be
// called during commit phase.
workInProgress.callbackList = workInProgress.updateQueue;
if (current) {
current.callbackList = workInProgress.callbackList;
}
markForPostEffect(workInProgress);
return null;
case HostContainer:
@@ -144,9 +144,6 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
// to the system not to redo any work here.
workInProgress.pendingProps = null;
workInProgress.updateQueue = null;
if (current) {
current.updateQueue = null;
}
const returnFiber = workInProgress.return;
@@ -654,6 +654,44 @@ describe('ReactIncremental', () => {
expect(instance.state.num).toEqual(6);
});
it('can call setState inside update callback', () => {
let instance;
class Bar extends React.Component {
constructor() {
super();
this.state = { num: 1 };
instance = this;
}
render() {
return <div>{this.props.children}</div>;
}
}
function Foo({ multiplier }) {
return (
<div>
<Bar multiplier={multiplier} />
</div>
);
}
function updater(state, props) {
return { num: state.num * props.multiplier };
}
function callback() {
this.setState({ called: true });
}
ReactNoop.render(<Foo multiplier={2} />);
ReactNoop.flush();
instance.setState(updater);
instance.setState(updater, callback);
ReactNoop.flush();
expect(instance.state.num).toEqual(4);
expect(instance.state.called).toEqual(true);
});
it('can replaceState', () => {
let instance;
const Bar = React.createClass({