Fix native event batching in concurrent mode (#21010)

* Fix native event batching in concurrent mode

* Wrap DevTools test updates with act

These tests expect the `scheduleUpdate` DevTools hook to trigger a
synchronous re-render with legacy semantics, but flushing in a microtask
is fine. Wrapping the updates with `act` fixes it.

* Testing nits

* Nit: Check executionContext === NoContext first

In the common case it will be false and the binary expression will
short circuit.

Co-authored-by: Andrew Clark <git@andrewclark.io>
This commit is contained in:
Ricky
2021-03-16 13:15:00 -04:00
committed by GitHub
parent 0203b6567c
commit 89acfa639b
4 changed files with 70 additions and 16 deletions
@@ -201,32 +201,32 @@ describe('React hooks DevTools integration', () => {
if (__DEV__) {
// First render was locked
expect(renderer.toJSON().children).toEqual(['Loading']);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);
// Release the lock
setSuspenseHandler(() => false);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
// Lock again
setSuspenseHandler(() => true);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);
// Release the lock again
setSuspenseHandler(() => false);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
// Ensure it checks specific fibers.
setSuspenseHandler(f => f === fiber || f === fiber.alternate);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);
setSuspenseHandler(f => f !== fiber && f !== fiber.alternate);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
} else {
expect(renderer.toJSON().children).toEqual(['Done']);
@@ -259,33 +259,33 @@ describe('React hooks DevTools integration', () => {
if (__DEV__) {
// First render was locked
expect(renderer.toJSON().children).toEqual(['Loading']);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);
// Release the lock
setSuspenseHandler(() => false);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
Scheduler.unstable_flushAll();
expect(renderer.toJSON().children).toEqual(['Done']);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
// Lock again
setSuspenseHandler(() => true);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);
// Release the lock again
setSuspenseHandler(() => false);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
// Ensure it checks specific fibers.
setSuspenseHandler(f => f === fiber || f === fiber.alternate);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Loading']);
setSuspenseHandler(f => f !== fiber && f !== fiber.alternate);
scheduleUpdate(fiber); // Re-render
act(() => scheduleUpdate(fiber)); // Re-render
expect(renderer.toJSON().children).toEqual(['Done']);
} else {
expect(renderer.toJSON().children).toEqual(['Done']);
@@ -281,4 +281,52 @@ describe('ReactDOMNativeEventHeuristic-test', () => {
expect(container.textContent).toEqual('hovered');
});
});
// @gate experimental
it('should batch inside native events', async () => {
const root = ReactDOM.unstable_createRoot(container);
const target = React.createRef(null);
function Foo() {
const [count, setCount] = React.useState(0);
const countRef = React.useRef(-1);
React.useLayoutEffect(() => {
countRef.current = count;
target.current.onclick = () => {
setCount(countRef.current + 1);
// Now update again. If these updates are batched, then this should be
// a no-op, because we didn't re-render yet and `countRef` hasn't
// been mutated.
setCount(countRef.current + 1);
};
});
return <div ref={target}>Count: {count}</div>;
}
await act(async () => {
root.render(<Foo />);
});
expect(container.textContent).toEqual('Count: 0');
// Ignore act warning. We can't use act because it forces batched updates.
spyOnDev(console, 'error');
const pressEvent = document.createEvent('Event');
pressEvent.initEvent('click', true, true);
dispatchAndSetCurrentEvent(target.current, pressEvent);
// If this is 2, that means the `setCount` calls were not batched.
expect(container.textContent).toEqual('Count: 1');
// Assert that the `act` warnings were the only ones that fired.
if (__DEV__) {
expect(console.error).toHaveBeenCalledTimes(2);
expect(console.error.calls.argsFor(0)[0]).toContain(
'was not wrapped in act',
);
expect(console.error.calls.argsFor(1)[0]).toContain(
'was not wrapped in act',
);
}
});
});
@@ -547,7 +547,10 @@ export function scheduleUpdateOnFiber(
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
if (
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode
) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
@@ -547,7 +547,10 @@ export function scheduleUpdateOnFiber(
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
if (
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode
) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback