Skip to content

Commit 12f1851

Browse files
committed
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).
1 parent ed6c091 commit 12f1851

File tree

7 files changed

+57
-18
lines changed

7 files changed

+57
-18
lines changed

packages/react-dom/src/test-utils/ReactTestUtils.js

+1-7
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,8 @@ const getNodeFromInstance = EventInternals[1];
3333
const getFiberCurrentPropsFromNode = EventInternals[2];
3434
const enqueueStateRestore = EventInternals[3];
3535
const restoreStateIfNeeded = EventInternals[4];
36-
const batchedUpdates = EventInternals[5];
3736

38-
const act_notBatchedInLegacyMode = React.unstable_act;
39-
function act(callback) {
40-
return act_notBatchedInLegacyMode(() => {
41-
return batchedUpdates(callback);
42-
});
43-
}
37+
const act = React.unstable_act;
4438

4539
function Event(suffix) {}
4640

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,9 @@ export function scheduleUpdateOnFiber(
518518
if (
519519
lane === SyncLane &&
520520
executionContext === NoContext &&
521-
(fiber.mode & ConcurrentMode) === NoMode
521+
(fiber.mode & ConcurrentMode) === NoMode &&
522+
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
523+
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
522524
) {
523525
// Flush the synchronous work now, unless we're already working or inside
524526
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
@@ -1049,7 +1051,11 @@ export function batchedUpdates<A, R>(fn: A => R, a: A): R {
10491051
executionContext = prevExecutionContext;
10501052
// If there were legacy sync updates, flush them at the end of the outer
10511053
// most batchedUpdates-like method.
1052-
if (executionContext === NoContext) {
1054+
if (
1055+
executionContext === NoContext &&
1056+
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
1057+
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
1058+
) {
10531059
resetRenderTimer();
10541060
flushSyncCallbacksOnlyInLegacyMode();
10551061
}

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,9 @@ export function scheduleUpdateOnFiber(
518518
if (
519519
lane === SyncLane &&
520520
executionContext === NoContext &&
521-
(fiber.mode & ConcurrentMode) === NoMode
521+
(fiber.mode & ConcurrentMode) === NoMode &&
522+
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
523+
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
522524
) {
523525
// Flush the synchronous work now, unless we're already working or inside
524526
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
@@ -1049,7 +1051,11 @@ export function batchedUpdates<A, R>(fn: A => R, a: A): R {
10491051
executionContext = prevExecutionContext;
10501052
// If there were legacy sync updates, flush them at the end of the outer
10511053
// most batchedUpdates-like method.
1052-
if (executionContext === NoContext) {
1054+
if (
1055+
executionContext === NoContext &&
1056+
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
1057+
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
1058+
) {
10531059
resetRenderTimer();
10541060
flushSyncCallbacksOnlyInLegacyMode();
10551061
}

packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js

+28
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,32 @@ describe('isomorphic act()', () => {
7878
});
7979
expect(returnValue).toEqual('hi');
8080
});
81+
82+
// @gate __DEV__
83+
test('in legacy mode, updates are batched', () => {
84+
const root = ReactNoop.createLegacyRoot();
85+
86+
// Outside of `act`, legacy updates are flushed completely synchronously
87+
root.render('A');
88+
expect(root).toMatchRenderedOutput('A');
89+
90+
// `act` will batch the updates and flush them at the end
91+
act(() => {
92+
root.render('B');
93+
// Hasn't flushed yet
94+
expect(root).toMatchRenderedOutput('A');
95+
96+
// Confirm that a nested `batchedUpdates` call won't cause the updates
97+
// to flush early.
98+
ReactNoop.batchedUpdates(() => {
99+
root.render('C');
100+
});
101+
102+
// Still hasn't flushed
103+
expect(root).toMatchRenderedOutput('A');
104+
});
105+
106+
// Now everything renders in a single batch.
107+
expect(root).toMatchRenderedOutput('C');
108+
});
81109
});

packages/react-test-renderer/src/ReactTestRenderer.js

+1-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
* @flow
88
*/
99

10-
import type {Thenable} from 'shared/ReactTypes';
1110
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
1211
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
1312
import type {Instance, TextInstance} from './ReactTestHostConfig';
@@ -50,12 +49,7 @@ import {getPublicInstance} from './ReactTestHostConfig';
5049
import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
5150
import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
5251

53-
const act_notBatchedInLegacyMode = React.unstable_act;
54-
function act<T>(callback: () => T): Thenable<T> {
55-
return act_notBatchedInLegacyMode(() => {
56-
return batchedUpdates(callback);
57-
});
58-
}
52+
const act = React.unstable_act;
5953

6054
// TODO: Remove from public bundle
6155

packages/react/src/ReactAct.js

+8
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,20 @@ export function act<T>(callback: () => T | Thenable<T>): Thenable<T> {
2828
ReactCurrentActQueue.current = [];
2929
}
3030

31+
const prevIsBatchingLegacy = ReactCurrentActQueue.isBatchingLegacy;
3132
let result;
3233
try {
34+
// Used to reproduce behavior of `batchedUpdates` in legacy mode. Only
35+
// set to `true` while the given callback is executed, not for updates
36+
// triggered during an async event, because this is how the legacy
37+
// implementation of `act` behaved.
38+
ReactCurrentActQueue.isBatchingLegacy = true;
3339
result = callback();
3440
} catch (error) {
3541
popActScope(prevActScopeDepth);
3642
throw error;
43+
} finally {
44+
ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy;
3745
}
3846

3947
if (

packages/react/src/ReactCurrentActQueue.js

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const ReactCurrentActQueue = {
1717
// on at the testing frameworks layer? Instead of what we do now, which
1818
// is check if a `jest` global is defined.
1919
disableActWarning: (false: boolean),
20+
21+
// Used to reproduce behavior of `batchedUpdates` in legacy mode.
22+
isBatchingLegacy: false,
2023
};
2124

2225
export default ReactCurrentActQueue;

0 commit comments

Comments
 (0)