Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

act: Batch updates, even in legacy roots #21797

Merged
merged 1 commit into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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).
  • Loading branch information
acdlite committed Jul 12, 2021
commit a2fe8704c299278f292db83b987e82c93d911a1b
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 1 addition & 7 deletions packages/react-dom/src/test-utils/ReactTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down
13 changes: 11 additions & 2 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -668,6 +670,9 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) {
ReactCurrentActQueue.didScheduleLegacyUpdate = true;
}
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
Expand Down Expand Up @@ -1049,7 +1054,11 @@ export function batchedUpdates<A, R>(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();
}
Expand Down
13 changes: 11 additions & 2 deletions packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -668,6 +670,9 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) {
ReactCurrentActQueue.didScheduleLegacyUpdate = true;
}
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
Expand Down Expand Up @@ -1049,7 +1054,11 @@ export function batchedUpdates<A, R>(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();
}
Expand Down
49 changes: 49 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,53 @@ 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');
});

// @gate __DEV__
test('in legacy mode, in an async scope, updates are batched until the first `await`', async () => {
const root = ReactNoop.createLegacyRoot();

await act(async () => {
// These updates are batched. This replicates the behavior of the original
// `act` implementation, for compatibility.
root.render('A');
root.render('B');
// Nothing has rendered yet.
expect(root).toMatchRenderedOutput(null);
await null;
// Updates are flushed after the first await.
expect(root).toMatchRenderedOutput('B');

// Subsequent updates in the same scope aren't batched.
root.render('C');
expect(root).toMatchRenderedOutput('C');
});
});
});
8 changes: 1 addition & 7 deletions packages/react-test-renderer/src/ReactTestRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<T>(callback: () => T): Thenable<T> {
return act_notBatchedInLegacyMode(() => {
return batchedUpdates(callback);
});
}
const act = React.unstable_act;

// TODO: Remove from public bundle

Expand Down
22 changes: 22 additions & 0 deletions packages/react/src/ReactAct.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,34 @@ export function act<T>(callback: () => T | Thenable<T>): Thenable<T> {
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();

// Replicate behavior of original `act` implementation in legacy mode,
// which flushed updates immediately after the scope function exits, even
// if it's an async function.
if (
!prevIsBatchingLegacy &&
ReactCurrentActQueue.didScheduleLegacyUpdate
) {
const queue = ReactCurrentActQueue.current;
if (queue !== null) {
ReactCurrentActQueue.didScheduleLegacyUpdate = false;
flushActQueue(queue);
}
}
} catch (error) {
popActScope(prevActScopeDepth);
throw error;
} finally {
ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy;
}

if (
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/ReactCurrentActQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ 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,
didScheduleLegacyUpdate: false,
};

export default ReactCurrentActQueue;