Sync setStates inside willUpdate and didUpdate should flush at same time (#11212)

In sync mode, we downgrade sync priority work to task work when we're in
the commit phase, but not in the render phase. That means if you
schedule updates in both phases, the render phase update will flush
first, and the commit phase update will flush after that. What should
really happen is that both updates flush at the same time.

To solve this, updates in the commit phase are now given sync priority.
The distinction between task and sync really only exists to account for
a historical quirk in the behavior of top-level mounts. (Refer to the
test case titled "initial mount is sync inside batchedUpdates".)

Ideally, there would only be one priority for both sync and task. This
gets us closer to that model, while still accounting for
top-level mounts.
This commit is contained in:
Andrew Clark
2017-10-12 21:05:58 -07:00
committed by GitHub
parent 0c5a455ecb
commit 339d6cb32b
2 changed files with 107 additions and 2 deletions
@@ -1072,6 +1072,108 @@ describe('ReactUpdates', () => {
expect(ops).toEqual(['Hello', '']);
});
it(
'in sync mode, updates in componentWillUpdate and componentDidUpdate ' +
'should both flush in the immediately subsequent commit',
() => {
let ops = [];
class Foo extends React.Component {
state = {a: false, b: false};
componentWillUpdate(_, nextState) {
if (!nextState.a) {
this.setState({a: true});
}
}
componentDidUpdate() {
ops.push('Foo updated');
if (!this.state.b) {
this.setState({b: true});
}
}
render() {
ops.push(`a: ${this.state.a}, b: ${this.state.b}`);
return null;
}
}
const container = document.createElement('div');
// Mount
ReactDOM.render(<Foo />, container);
// Root update
ReactDOM.render(<Foo />, container);
expect(ops).toEqual([
// Mount
'a: false, b: false',
// Root update
'a: false, b: false',
'Foo updated',
// Subsequent update (both a and b should have flushed)
'a: true, b: true',
'Foo updated',
// There should not be any additional updates
]);
},
);
it(
'in sync mode, updates in componentWillUpdate and componentDidUpdate ' +
'(on a sibling) should both flush in the immediately subsequent commit',
() => {
let ops = [];
class Foo extends React.Component {
state = {a: false};
componentWillUpdate(_, nextState) {
if (!nextState.a) {
this.setState({a: true});
}
}
componentDidUpdate() {
ops.push('Foo updated');
}
render() {
ops.push(`a: ${this.state.a}`);
return null;
}
}
class Bar extends React.Component {
state = {b: false};
componentDidUpdate() {
ops.push('Bar updated');
if (!this.state.b) {
this.setState({b: true});
}
}
render() {
ops.push(`b: ${this.state.b}`);
return null;
}
}
const container = document.createElement('div');
// Mount
ReactDOM.render(<div><Foo /><Bar /></div>, container);
// Root update
ReactDOM.render(<div><Foo /><Bar /></div>, container);
expect(ops).toEqual([
// Mount
'a: false',
'b: false',
// Root update
'a: false',
'b: false',
'Foo updated',
'Bar updated',
// Subsequent update (both a and b should have flushed)
'a: true',
'b: true',
'Foo updated',
'Bar updated',
// There should not be any additional updates
]);
},
);
it('does not re-render if state update is null', () => {
let container = document.createElement('div');
@@ -1494,7 +1494,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
if (isCommitting) {
// Updates that occur during the commit phase should have task priority
// by default.
expirationTime = Task;
expirationTime = Sync;
} else {
// Updates during the render phase should expire at the same time as
// the work that is being rendered.
@@ -1517,7 +1517,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}
}
if (expirationTime === Sync && (isCommitting || isBatchingUpdates)) {
if (
expirationTime === Sync &&
(isBatchingUpdates || (isUnbatchingUpdates && isCommitting))
) {
// If we're in a batch, or in the commit phase, downgrade sync to task
return Task;
}