Skip to content

Commit 163e81c

Browse files
authored
Support disabling spurious act warnings with a global environment flag (#22561)
* Extract `act` environment check into function `act` checks the environment to determine whether to fire a warning. We're changing how this check works in React 18. As a first step, this refactors the logic into a single function. No behavior changes yet. * Use IS_REACT_ACT_ENVIRONMENT to disable warnings If `IS_REACT_ACT_ENVIRONMENT` is set to `false`, we will suppress any `act` warnings. Otherwise, the behavior of `act` is the same as in React 17: if `jest` is defined, it warns. In concurrent mode, the plan is to remove the `jest` check and only warn if `IS_REACT_ACT_ENVIRONMENT` is true. I have not implemented that part yet.
1 parent b72dc8e commit 163e81c

16 files changed

+142
-60
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,5 +279,6 @@ module.exports = {
279279
__VARIANT__: true,
280280
gate: true,
281281
trustedTypes: true,
282+
IS_REACT_ACT_ENVIRONMENT: true,
282283
},
283284
};

packages/jest-react/src/internalAct.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ import type {Thenable} from 'shared/ReactTypes';
1818

1919
import * as Scheduler from 'scheduler/unstable_mock';
2020

21-
import ReactSharedInternals from 'shared/ReactSharedInternals';
2221
import enqueueTask from 'shared/enqueueTask';
23-
const {ReactCurrentActQueue} = ReactSharedInternals;
2422

2523
let actingUpdatesScopeDepth = 0;
2624

@@ -37,15 +35,18 @@ export function act(scope: () => Thenable<mixed> | void) {
3735
);
3836
}
3937

38+
const previousIsActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
4039
const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
4140
actingUpdatesScopeDepth++;
4241
if (__DEV__ && actingUpdatesScopeDepth === 1) {
43-
ReactCurrentActQueue.disableActWarning = true;
42+
// Because this is not the "real" `act`, we set this to `false` so React
43+
// knows not to fire `act` warnings.
44+
global.IS_REACT_ACT_ENVIRONMENT = false;
4445
}
4546

4647
const unwind = () => {
4748
if (__DEV__ && actingUpdatesScopeDepth === 1) {
48-
ReactCurrentActQueue.disableActWarning = false;
49+
global.IS_REACT_ACT_ENVIRONMENT = previousIsActEnvironment;
4950
}
5051
actingUpdatesScopeDepth--;
5152

packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,40 @@ function runActTests(label, render, unmount, rerender) {
273273
]);
274274
});
275275

276+
// @gate __DEV__
277+
it('does not warn if IS_REACT_ACT_ENVIRONMENT is set to false', () => {
278+
let setState;
279+
function App() {
280+
const [state, _setState] = React.useState(0);
281+
setState = _setState;
282+
return state;
283+
}
284+
285+
act(() => {
286+
render(<App />, container);
287+
});
288+
289+
// First show that it does warn
290+
expect(() => setState(1)).toErrorDev(
291+
'An update to App inside a test was not wrapped in act(...)',
292+
);
293+
294+
// Now do the same thing again, but disable with the environment flag
295+
const prevIsActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
296+
global.IS_REACT_ACT_ENVIRONMENT = false;
297+
try {
298+
setState(2);
299+
} finally {
300+
global.IS_REACT_ACT_ENVIRONMENT = prevIsActEnvironment;
301+
}
302+
303+
// When the flag is restored to its previous value, it should start
304+
// warning again. This shows that React reads the flag each time.
305+
expect(() => setState(3)).toErrorDev(
306+
'An update to App inside a test was not wrapped in act(...)',
307+
);
308+
});
309+
276310
describe('fake timers', () => {
277311
beforeEach(() => {
278312
jest.useFakeTimers();
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {Fiber} from './ReactFiber.new';
11+
import {warnsIfNotActing} from './ReactFiberHostConfig';
12+
13+
export function isActEnvironment(fiber: Fiber) {
14+
if (__DEV__) {
15+
const isReactActEnvironmentGlobal =
16+
// $FlowExpectedError – Flow doesn't know about IS_REACT_ACT_ENVIRONMENT global
17+
typeof IS_REACT_ACT_ENVIRONMENT !== 'undefined'
18+
? IS_REACT_ACT_ENVIRONMENT
19+
: undefined;
20+
21+
// TODO: Only check `jest` in legacy mode. In concurrent mode, this
22+
// heuristic is replaced by IS_REACT_ACT_ENVIRONMENT.
23+
// $FlowExpectedError - Flow doesn't know about jest
24+
const jestIsDefined = typeof jest !== 'undefined';
25+
return (
26+
warnsIfNotActing &&
27+
jestIsDefined &&
28+
// Legacy mode assumes an act environment whenever `jest` is defined, but
29+
// you can still turn off spurious warnings by setting
30+
// IS_REACT_ACT_ENVIRONMENT explicitly to false.
31+
isReactActEnvironmentGlobal !== false
32+
);
33+
}
34+
return false;
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {Fiber} from './ReactFiber.old';
11+
import {warnsIfNotActing} from './ReactFiberHostConfig';
12+
13+
export function isActEnvironment(fiber: Fiber) {
14+
if (__DEV__) {
15+
const isReactActEnvironmentGlobal =
16+
// $FlowExpectedError – Flow doesn't know about IS_REACT_ACT_ENVIRONMENT global
17+
typeof IS_REACT_ACT_ENVIRONMENT !== 'undefined'
18+
? IS_REACT_ACT_ENVIRONMENT
19+
: undefined;
20+
21+
// TODO: Only check `jest` in legacy mode. In concurrent mode, this
22+
// heuristic is replaced by IS_REACT_ACT_ENVIRONMENT.
23+
// $FlowExpectedError - Flow doesn't know about jest
24+
const jestIsDefined = typeof jest !== 'undefined';
25+
return (
26+
warnsIfNotActing &&
27+
jestIsDefined &&
28+
// Legacy mode assumes an act environment whenever `jest` is defined, but
29+
// you can still turn off spurious warnings by setting
30+
// IS_REACT_ACT_ENVIRONMENT explicitly to false.
31+
isReactActEnvironmentGlobal !== false
32+
);
33+
}
34+
return false;
35+
}

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ import {
118118
} from './ReactUpdateQueue.new';
119119
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
120120
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
121+
import {isActEnvironment} from './ReactFiberAct.new';
121122

122123
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
123124

@@ -1678,8 +1679,7 @@ function mountEffect(
16781679
deps: Array<mixed> | void | null,
16791680
): void {
16801681
if (__DEV__) {
1681-
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
1682-
if ('undefined' !== typeof jest) {
1682+
if (isActEnvironment(currentlyRenderingFiber)) {
16831683
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
16841684
}
16851685
}
@@ -1709,8 +1709,7 @@ function updateEffect(
17091709
deps: Array<mixed> | void | null,
17101710
): void {
17111711
if (__DEV__) {
1712-
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
1713-
if ('undefined' !== typeof jest) {
1712+
if (isActEnvironment(currentlyRenderingFiber)) {
17141713
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
17151714
}
17161715
}
@@ -2193,8 +2192,7 @@ function dispatchReducerAction<S, A>(
21932192
enqueueUpdate(fiber, queue, update, lane);
21942193

21952194
if (__DEV__) {
2196-
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
2197-
if ('undefined' !== typeof jest) {
2195+
if (isActEnvironment(fiber)) {
21982196
warnIfNotCurrentlyActingUpdatesInDev(fiber);
21992197
}
22002198
}
@@ -2279,8 +2277,7 @@ function dispatchSetState<S, A>(
22792277
}
22802278
}
22812279
if (__DEV__) {
2282-
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
2283-
if ('undefined' !== typeof jest) {
2280+
if (isActEnvironment(fiber)) {
22842281
warnIfNotCurrentlyActingUpdatesInDev(fiber);
22852282
}
22862283
}

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ import {
118118
} from './ReactUpdateQueue.old';
119119
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
120120
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
121+
import {isActEnvironment} from './ReactFiberAct.old';
121122

122123
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
123124

@@ -1678,8 +1679,7 @@ function mountEffect(
16781679
deps: Array<mixed> | void | null,
16791680
): void {
16801681
if (__DEV__) {
1681-
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
1682-
if ('undefined' !== typeof jest) {
1682+
if (isActEnvironment(currentlyRenderingFiber)) {
16831683
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
16841684
}
16851685
}
@@ -1709,8 +1709,7 @@ function updateEffect(
17091709
deps: Array<mixed> | void | null,
17101710
): void {
17111711
if (__DEV__) {
1712-
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
1713-
if ('undefined' !== typeof jest) {
1712+
if (isActEnvironment(currentlyRenderingFiber)) {
17141713
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
17151714
}
17161715
}
@@ -2193,8 +2192,7 @@ function dispatchReducerAction<S, A>(
21932192
enqueueUpdate(fiber, queue, update, lane);
21942193

21952194
if (__DEV__) {
2196-
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
2197-
if ('undefined' !== typeof jest) {
2195+
if (isActEnvironment(fiber)) {
21982196
warnIfNotCurrentlyActingUpdatesInDev(fiber);
21992197
}
22002198
}
@@ -2279,8 +2277,7 @@ function dispatchSetState<S, A>(
22792277
}
22802278
}
22812279
if (__DEV__) {
2282-
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
2283-
if ('undefined' !== typeof jest) {
2280+
if (isActEnvironment(fiber)) {
22842281
warnIfNotCurrentlyActingUpdatesInDev(fiber);
22852282
}
22862283
}

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

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ import {
8484
scheduleTimeout,
8585
cancelTimeout,
8686
noTimeout,
87-
warnsIfNotActing,
8887
afterActiveInstanceBlur,
8988
clearContainer,
9089
getCurrentEventPriority,
@@ -2816,15 +2815,8 @@ function shouldForceFlushFallbacksInDEV() {
28162815
export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
28172816
if (__DEV__) {
28182817
if (
2819-
warnsIfNotActing === true &&
28202818
(fiber.mode & StrictLegacyMode) !== NoMode &&
2821-
ReactCurrentActQueue.current === null &&
2822-
// Our internal tests use a custom implementation of `act` that works by
2823-
// mocking the Scheduler package. Disable the `act` warning.
2824-
// TODO: Maybe the warning should be disabled by default, and then turned
2825-
// on at the testing frameworks layer? Instead of what we do now, which
2826-
// is check if a `jest` global is defined.
2827-
ReactCurrentActQueue.disableActWarning === false
2819+
ReactCurrentActQueue.current === null
28282820
) {
28292821
console.error(
28302822
'An update to %s ran an effect, but was not wrapped in act(...).\n\n' +
@@ -2846,15 +2838,8 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
28462838
function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
28472839
if (__DEV__) {
28482840
if (
2849-
warnsIfNotActing === true &&
28502841
executionContext === NoContext &&
2851-
ReactCurrentActQueue.current === null &&
2852-
// Our internal tests use a custom implementation of `act` that works by
2853-
// mocking the Scheduler package. Disable the `act` warning.
2854-
// TODO: Maybe the warning should be disabled by default, and then turned
2855-
// on at the testing frameworks layer? Instead of what we do now, which
2856-
// is check if a `jest` global is defined.
2857-
ReactCurrentActQueue.disableActWarning === false
2842+
ReactCurrentActQueue.current === null
28582843
) {
28592844
const previousFiber = ReactCurrentFiberCurrent;
28602845
try {

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

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ import {
8484
scheduleTimeout,
8585
cancelTimeout,
8686
noTimeout,
87-
warnsIfNotActing,
8887
afterActiveInstanceBlur,
8988
clearContainer,
9089
getCurrentEventPriority,
@@ -2816,15 +2815,8 @@ function shouldForceFlushFallbacksInDEV() {
28162815
export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
28172816
if (__DEV__) {
28182817
if (
2819-
warnsIfNotActing === true &&
28202818
(fiber.mode & StrictLegacyMode) !== NoMode &&
2821-
ReactCurrentActQueue.current === null &&
2822-
// Our internal tests use a custom implementation of `act` that works by
2823-
// mocking the Scheduler package. Disable the `act` warning.
2824-
// TODO: Maybe the warning should be disabled by default, and then turned
2825-
// on at the testing frameworks layer? Instead of what we do now, which
2826-
// is check if a `jest` global is defined.
2827-
ReactCurrentActQueue.disableActWarning === false
2819+
ReactCurrentActQueue.current === null
28282820
) {
28292821
console.error(
28302822
'An update to %s ran an effect, but was not wrapped in act(...).\n\n' +
@@ -2846,15 +2838,8 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
28462838
function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
28472839
if (__DEV__) {
28482840
if (
2849-
warnsIfNotActing === true &&
28502841
executionContext === NoContext &&
2851-
ReactCurrentActQueue.current === null &&
2852-
// Our internal tests use a custom implementation of `act` that works by
2853-
// mocking the Scheduler package. Disable the `act` warning.
2854-
// TODO: Maybe the warning should be disabled by default, and then turned
2855-
// on at the testing frameworks layer? Instead of what we do now, which
2856-
// is check if a `jest` global is defined.
2857-
ReactCurrentActQueue.disableActWarning === false
2842+
ReactCurrentActQueue.current === null
28582843
) {
28592844
const previousFiber = ReactCurrentFiberCurrent;
28602845
try {

packages/react/src/ReactCurrentActQueue.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@ type RendererTask = boolean => RendererTask | null;
1111

1212
const ReactCurrentActQueue = {
1313
current: (null: null | Array<RendererTask>),
14-
// Our internal tests use a custom implementation of `act` that works by
15-
// mocking the Scheduler package. Use this field to disable the `act` warning.
16-
// TODO: Maybe the warning should be disabled by default, and then turned
17-
// on at the testing frameworks layer? Instead of what we do now, which
18-
// is check if a `jest` global is defined.
19-
disableActWarning: (false: boolean),
2014

2115
// Used to reproduce behavior of `batchedUpdates` in legacy mode.
2216
isBatchingLegacy: false,

scripts/rollup/validate/eslintrc.cjs.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ module.exports = {
4242
// jest
4343
expect: true,
4444
jest: true,
45+
46+
// act
47+
IS_REACT_ACT_ENVIRONMENT: true,
4548
},
4649
parserOptions: {
4750
ecmaVersion: 5,

scripts/rollup/validate/eslintrc.cjs2015.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ module.exports = {
4242
// jest
4343
expect: true,
4444
jest: true,
45+
46+
// act
47+
IS_REACT_ACT_ENVIRONMENT: true,
4548
},
4649
parserOptions: {
4750
ecmaVersion: 2015,

scripts/rollup/validate/eslintrc.esm.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ module.exports = {
4242
// jest
4343
expect: true,
4444
jest: true,
45+
46+
// act
47+
IS_REACT_ACT_ENVIRONMENT: true,
4548
},
4649
parserOptions: {
4750
ecmaVersion: 2017,

scripts/rollup/validate/eslintrc.fb.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ module.exports = {
3838

3939
// jest
4040
jest: true,
41+
42+
// act
43+
IS_REACT_ACT_ENVIRONMENT: true,
4144
},
4245
parserOptions: {
4346
ecmaVersion: 5,

scripts/rollup/validate/eslintrc.rn.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ module.exports = {
3434

3535
// jest
3636
jest: true,
37+
38+
// act
39+
IS_REACT_ACT_ENVIRONMENT: true,
3740
},
3841
parserOptions: {
3942
ecmaVersion: 5,

0 commit comments

Comments
 (0)