From e15772506213cb815aac1b005948d4d30f97cc89 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sat, 3 Jul 2021 11:15:58 -0400 Subject: [PATCH] act: Batch updates, even in legacy roots In legacy roots, if an update originates outside of `batchedUpdates`, check if it's inside an `act` scope; if so, treat it as if it were batched. This is only necessary in legacy roots because in concurrent roots, updates are batched by default. With this change, the Test Utils and Test Renderer versions of `act` are nothing more than aliases of the isomorphic API (still not exposed, but will likely be the recommended API that replaces the others). --- .../src/__tests__/inspectedElement-test.js | 2 +- .../src/test-utils/ReactTestUtils.js | 8 +----- .../src/ReactFiberWorkLoop.new.js | 10 +++++-- .../src/ReactFiberWorkLoop.old.js | 10 +++++-- .../src/__tests__/ReactIsomorphicAct-test.js | 28 +++++++++++++++++++ .../src/ReactTestRenderer.js | 8 +----- packages/react/src/ReactAct.js | 8 ++++++ packages/react/src/ReactCurrentActQueue.js | 3 ++ 8 files changed, 58 insertions(+), 19 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index 4eb4b76a07e7e..67598900ec906 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -2462,7 +2462,7 @@ describe('InspectedElement', () => { }; const toggleError = async forceError => { await withErrorsOrWarningsIgnored(['ErrorBoundary'], async () => { - await utils.actAsync(() => { + await TestUtilsAct(() => { bridge.send('overrideError', { id: targetErrorBoundaryID, rendererID: store.getRendererIDForElement(targetErrorBoundaryID), diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index cc93dc866eaa8..f93c92a93767c 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -33,14 +33,8 @@ const getNodeFromInstance = EventInternals[1]; const getFiberCurrentPropsFromNode = EventInternals[2]; const enqueueStateRestore = EventInternals[3]; const restoreStateIfNeeded = EventInternals[4]; -const batchedUpdates = EventInternals[5]; -const act_notBatchedInLegacyMode = React.unstable_act; -function act(callback) { - return act_notBatchedInLegacyMode(() => { - return batchedUpdates(callback); - }); -} +const act = React.unstable_act; function Event(suffix) {} diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index bb855370faec2..467abf9381073 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -518,7 +518,9 @@ export function scheduleUpdateOnFiber( if ( lane === SyncLane && executionContext === NoContext && - (fiber.mode & ConcurrentMode) === NoMode + (fiber.mode & ConcurrentMode) === NoMode && + // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode. + !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy) ) { // Flush the synchronous work now, unless we're already working or inside // a batch. This is intentionally inside scheduleUpdateOnFiber instead of @@ -1049,7 +1051,11 @@ export function batchedUpdates(fn: A => R, a: A): R { executionContext = prevExecutionContext; // If there were legacy sync updates, flush them at the end of the outer // most batchedUpdates-like method. - if (executionContext === NoContext) { + if ( + executionContext === NoContext && + // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode. + !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy) + ) { resetRenderTimer(); flushSyncCallbacksOnlyInLegacyMode(); } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index a7b020726e713..11ec09be9618e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -518,7 +518,9 @@ export function scheduleUpdateOnFiber( if ( lane === SyncLane && executionContext === NoContext && - (fiber.mode & ConcurrentMode) === NoMode + (fiber.mode & ConcurrentMode) === NoMode && + // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode. + !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy) ) { // Flush the synchronous work now, unless we're already working or inside // a batch. This is intentionally inside scheduleUpdateOnFiber instead of @@ -1049,7 +1051,11 @@ export function batchedUpdates(fn: A => R, a: A): R { executionContext = prevExecutionContext; // If there were legacy sync updates, flush them at the end of the outer // most batchedUpdates-like method. - if (executionContext === NoContext) { + if ( + executionContext === NoContext && + // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode. + !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy) + ) { resetRenderTimer(); flushSyncCallbacksOnlyInLegacyMode(); } diff --git a/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js b/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js index 001474e935217..ab918df779584 100644 --- a/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js @@ -78,4 +78,32 @@ describe('isomorphic act()', () => { }); expect(returnValue).toEqual('hi'); }); + + // @gate __DEV__ + test('in legacy mode, updates are batched', () => { + const root = ReactNoop.createLegacyRoot(); + + // Outside of `act`, legacy updates are flushed completely synchronously + root.render('A'); + expect(root).toMatchRenderedOutput('A'); + + // `act` will batch the updates and flush them at the end + act(() => { + root.render('B'); + // Hasn't flushed yet + expect(root).toMatchRenderedOutput('A'); + + // Confirm that a nested `batchedUpdates` call won't cause the updates + // to flush early. + ReactNoop.batchedUpdates(() => { + root.render('C'); + }); + + // Still hasn't flushed + expect(root).toMatchRenderedOutput('A'); + }); + + // Now everything renders in a single batch. + expect(root).toMatchRenderedOutput('C'); + }); }); diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index eb00975506956..60e02766dc9dd 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -7,7 +7,6 @@ * @flow */ -import type {Thenable} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type {Instance, TextInstance} from './ReactTestHostConfig'; @@ -50,12 +49,7 @@ import {getPublicInstance} from './ReactTestHostConfig'; import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags'; import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags'; -const act_notBatchedInLegacyMode = React.unstable_act; -function act(callback: () => T): Thenable { - return act_notBatchedInLegacyMode(() => { - return batchedUpdates(callback); - }); -} +const act = React.unstable_act; // TODO: Remove from public bundle diff --git a/packages/react/src/ReactAct.js b/packages/react/src/ReactAct.js index 99156f85fde4c..bbc2200dc5dc2 100644 --- a/packages/react/src/ReactAct.js +++ b/packages/react/src/ReactAct.js @@ -28,12 +28,20 @@ export function act(callback: () => T | Thenable): Thenable { ReactCurrentActQueue.current = []; } + const prevIsBatchingLegacy = ReactCurrentActQueue.isBatchingLegacy; let result; try { + // Used to reproduce behavior of `batchedUpdates` in legacy mode. Only + // set to `true` while the given callback is executed, not for updates + // triggered during an async event, because this is how the legacy + // implementation of `act` behaved. + ReactCurrentActQueue.isBatchingLegacy = true; result = callback(); } catch (error) { popActScope(prevActScopeDepth); throw error; + } finally { + ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy; } if ( diff --git a/packages/react/src/ReactCurrentActQueue.js b/packages/react/src/ReactCurrentActQueue.js index 1b7966337cad5..e2438f146b8d4 100644 --- a/packages/react/src/ReactCurrentActQueue.js +++ b/packages/react/src/ReactCurrentActQueue.js @@ -17,6 +17,9 @@ const ReactCurrentActQueue = { // on at the testing frameworks layer? Instead of what we do now, which // is check if a `jest` global is defined. disableActWarning: (false: boolean), + + // Used to reproduce behavior of `batchedUpdates` in legacy mode. + isBatchingLegacy: false, }; export default ReactCurrentActQueue;