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.
Andrew Clark committed
Oct 18, 2021 at 11:27 UTC
163e81c1f88a47749e092f2d5b31b7c919f07de8
16 files changed
+142
-60
.eslintrc.js
+1
@@ -279,5 +279,6 @@ module.exports = {
279
__VARIANT__: true,
280
gate: true,
281
trustedTypes: true,
282
+ IS_REACT_ACT_ENVIRONMENT: true,
283
},
284
};
packages/jest-react/src/internalAct.js
+5
-4
@@ -18,9 +18,7 @@ import type {Thenable} from 'shared/ReactTypes';
18
19
import * as Scheduler from 'scheduler/unstable_mock';
20
21
-import ReactSharedInternals from 'shared/ReactSharedInternals';
21
import enqueueTask from 'shared/enqueueTask';
23
-const {ReactCurrentActQueue} = ReactSharedInternals;
22
23
let actingUpdatesScopeDepth = 0;
24
@@ -37,15 +35,18 @@ export function act(scope: () => Thenable<mixed> | void) {
35
);
36
}
37
38
+ const previousIsActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
39
const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
40
actingUpdatesScopeDepth++;
41
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;
45
}
46
47
const unwind = () => {
48
if (__DEV__ && actingUpdatesScopeDepth === 1) {
48
- ReactCurrentActQueue.disableActWarning = false;
49
+ global.IS_REACT_ACT_ENVIRONMENT = previousIsActEnvironment;
50
}
51
actingUpdatesScopeDepth--;
52
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+34
@@ -273,6 +273,40 @@ function runActTests(label, render, unmount, rerender) {
273
]);
274
});
275
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
+
310
describe('fake timers', () => {
311
beforeEach(() => {
312
jest.useFakeTimers();
packages/react-reconciler/src/ReactFiberAct.new.js
new
+35
@@ -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
+}
packages/react-reconciler/src/ReactFiberAct.old.js
new
+35
@@ -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
+5
-8
@@ -118,6 +118,7 @@ import {
118
} from './ReactUpdateQueue.new';
119
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
120
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
121
+import {isActEnvironment} from './ReactFiberAct.new';
122
123
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
124
@@ -1678,8 +1679,7 @@ function mountEffect(
1679
deps: Array<mixed> | void | null,
1680
): void {
1681
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)) {
1683
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1684
}
1685
}
@@ -1709,8 +1709,7 @@ function updateEffect(
1709
deps: Array<mixed> | void | null,
1710
): void {
1711
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)) {
1713
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1714
}
1715
}
@@ -2193,8 +2192,7 @@ function dispatchReducerAction<S, A>(
2192
enqueueUpdate(fiber, queue, update, lane);
2193
2194
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)) {
2196
warnIfNotCurrentlyActingUpdatesInDev(fiber);
2197
}
2198
}
@@ -2279,8 +2277,7 @@ function dispatchSetState<S, A>(
2277
}
2278
}
2279
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)) {
2281
warnIfNotCurrentlyActingUpdatesInDev(fiber);
2282
}
2283
}
packages/react-reconciler/src/ReactFiberHooks.old.js
+5
-8
@@ -118,6 +118,7 @@ import {
118
} from './ReactUpdateQueue.old';
119
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
120
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
121
+import {isActEnvironment} from './ReactFiberAct.old';
122
123
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
124
@@ -1678,8 +1679,7 @@ function mountEffect(
1679
deps: Array<mixed> | void | null,
1680
): void {
1681
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)) {
1683
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1684
}
1685
}
@@ -1709,8 +1709,7 @@ function updateEffect(
1709
deps: Array<mixed> | void | null,
1710
): void {
1711
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)) {
1713
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
1714
}
1715
}
@@ -2193,8 +2192,7 @@ function dispatchReducerAction<S, A>(
2192
enqueueUpdate(fiber, queue, update, lane);
2193
2194
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)) {
2196
warnIfNotCurrentlyActingUpdatesInDev(fiber);
2197
}
2198
}
@@ -2279,8 +2277,7 @@ function dispatchSetState<S, A>(
2277
}
2278
}
2279
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)) {
2281
warnIfNotCurrentlyActingUpdatesInDev(fiber);
2282
}
2283
}
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+2
-17
@@ -84,7 +84,6 @@ import {
84
scheduleTimeout,
85
cancelTimeout,
86
noTimeout,
87
- warnsIfNotActing,
87
afterActiveInstanceBlur,
88
clearContainer,
89
getCurrentEventPriority,
@@ -2816,15 +2815,8 @@ function shouldForceFlushFallbacksInDEV() {
2815
export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
2816
if (__DEV__) {
2817
if (
2819
- warnsIfNotActing === true &&
2818
(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
2820
) {
2821
console.error(
2822
'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 {
2838
function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2839
if (__DEV__) {
2840
if (
2849
- warnsIfNotActing === true &&
2841
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
2843
) {
2844
const previousFiber = ReactCurrentFiberCurrent;
2845
try {
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+2
-17
@@ -84,7 +84,6 @@ import {
84
scheduleTimeout,
85
cancelTimeout,
86
noTimeout,
87
- warnsIfNotActing,
87
afterActiveInstanceBlur,
88
clearContainer,
89
getCurrentEventPriority,
@@ -2816,15 +2815,8 @@ function shouldForceFlushFallbacksInDEV() {
2815
export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
2816
if (__DEV__) {
2817
if (
2819
- warnsIfNotActing === true &&
2818
(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
2820
) {
2821
console.error(
2822
'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 {
2838
function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2839
if (__DEV__) {
2840
if (
2849
- warnsIfNotActing === true &&
2841
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
2843
) {
2844
const previousFiber = ReactCurrentFiberCurrent;
2845
try {
packages/react/src/ReactCurrentActQueue.js
-6
@@ -11,12 +11,6 @@ type RendererTask = boolean => RendererTask | null;
11
12
const ReactCurrentActQueue = {
13
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),
14
15
// Used to reproduce behavior of `batchedUpdates` in legacy mode.
16
isBatchingLegacy: false,
scripts/rollup/validate/eslintrc.cjs.js
+3
@@ -42,6 +42,9 @@ module.exports = {
42
// jest
43
expect: true,
44
jest: true,
45
+
46
+ // act
47
+ IS_REACT_ACT_ENVIRONMENT: true,
48
},
49
parserOptions: {
50
ecmaVersion: 5,
scripts/rollup/validate/eslintrc.cjs2015.js
+3
@@ -42,6 +42,9 @@ module.exports = {
42
// jest
43
expect: true,
44
jest: true,
45
+
46
+ // act
47
+ IS_REACT_ACT_ENVIRONMENT: true,
48
},
49
parserOptions: {
50
ecmaVersion: 2015,
scripts/rollup/validate/eslintrc.esm.js
+3
@@ -42,6 +42,9 @@ module.exports = {
42
// jest
43
expect: true,
44
jest: true,
45
+
46
+ // act
47
+ IS_REACT_ACT_ENVIRONMENT: true,
48
},
49
parserOptions: {
50
ecmaVersion: 2017,
scripts/rollup/validate/eslintrc.fb.js
+3
@@ -38,6 +38,9 @@ module.exports = {
38
39
// jest
40
jest: true,
41
+
42
+ // act
43
+ IS_REACT_ACT_ENVIRONMENT: true,
44
},
45
parserOptions: {
46
ecmaVersion: 5,
scripts/rollup/validate/eslintrc.rn.js
+3
@@ -34,6 +34,9 @@ module.exports = {
34
35
// jest
36
jest: true,
37
+
38
+ // act
39
+ IS_REACT_ACT_ENVIRONMENT: true,
40
},
41
parserOptions: {
42
ecmaVersion: 5,
scripts/rollup/validate/eslintrc.umd.js
+3
@@ -47,6 +47,9 @@ module.exports = {
47
48
// jest
49
jest: true,
50
+
51
+ // act
52
+ IS_REACT_ACT_ENVIRONMENT: true,
53
},
54
parserOptions: {
55
ecmaVersion: 5,