Interactive updates shouldn't flush until the end of the outermost batch

Accounts for the case where an event is dispatched synchronously from
inside another event, like `el.focus`. I've added a test, but in general
we need more coverage around this area.
This commit is contained in:
Andrew Clark
2018-01-30 23:17:22 -08:00
parent 4cd253276a
commit fc5fdd2bdd
3 changed files with 70 additions and 11 deletions
+1 -10
View File
@@ -5,11 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {
batchedUpdates,
flushInteractiveUpdates,
interactiveUpdates,
} from 'events/ReactGenericBatching';
import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
import {runExtractedEventsInBatch} from 'events/EventPluginHub';
import {isFiberMounted} from 'react-reconciler/reflection';
import {HostRoot} from 'shared/ReactTypeOfWork';
@@ -166,11 +162,6 @@ export function trapCapturedEvent(topLevelType, handlerBaseName, element) {
}
function dispatchInteractiveEvent(topLevelType, nativeEvent) {
// If there are any pending interactive updates, synchronously flush them.
// This needs to happen before we read any handlers, because the effect of the
// previous event may affect which handlers are called during this event.
flushInteractiveUpdates();
// Increase the priority of updates inside this event.
interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
}
@@ -167,6 +167,56 @@ describe('SimpleEventPlugin', function() {
});
});
it('batches updates that occur as a result of a nested event dispatch', () => {
const container = document.createElement('div');
document.body.appendChild(container);
let ops = [];
let button;
class Button extends React.Component {
state = {count: 0};
increment = () =>
this.setState(state => ({
count: state.count + 1,
}));
componentDidUpdate() {
ops.push(`didUpdate - Count: ${this.state.count}`);
}
render() {
return (
<button
ref={el => (button = el)}
onFocus={this.increment}
onClick={() => {
// The focus call synchronously dispatches a nested event. All of
// the updates in this handler should be batched together.
this.increment();
button.focus();
this.increment();
}}>
Count: {this.state.count}
</button>
);
}
}
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
ReactDOM.render(<Button />, container);
expect(button.textContent).toEqual('Count: 0');
expect(ops).toEqual([]);
click();
// There should be exactly one update.
expect(ops).toEqual(['didUpdate - Count: 3']);
expect(button.textContent).toEqual('Count: 3');
});
describe('interactive events, in async mode', () => {
beforeEach(() => {
jest.resetModules();
+19 -1
View File
@@ -1807,13 +1807,31 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
if (isBatchingInteractiveUpdates) {
return fn(a, b);
}
// If there are any pending interactive updates, synchronously flush them.
// This needs to happen before we read any handlers, because the effect of
// the previous event may influence which handlers are called during
// this event.
if (
!isBatchingUpdates &&
!isRendering &&
lowestPendingInteractiveExpirationTime !== NoWork
) {
// Synchronously flush pending interactive updates.
performWork(lowestPendingInteractiveExpirationTime, false, null);
lowestPendingInteractiveExpirationTime = NoWork;
}
const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
const previousIsBatchingUpdates = isBatchingUpdates;
isBatchingInteractiveUpdates = true;
isBatchingUpdates = true;
try {
return fn(a, b);
} finally {
isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}
}