@samitouri / QOS-React-2 / commits / 163e81c1f8

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,