@samitouri / QOS-React-2 / commits / a457e02ae3

allow nested `act()`s from different renderers (#16039)

* allow nested `act()`s from different renderers There are usecases where multiple renderers need to oprate inside an act() scope - ReactDOM.render being used inside another component tree. The parent component will be rendered using ReactTestRenderer.create for a snapshot test or something. - a ReactDOM instance interacting with a ReactTestRenderer instance (like for the new devtools) This PR changes the way the acting sigils operate to allow for this. It keeps 2 booleans, one attached to React, one attached to the renderer. act() changes these values, and the workloop reads them to decide what warning to trigger. I also renamed shouldWarnUnactedUpdates to warnsIfNotActing * s/ReactIsActing/IsSomeRendererActing and s/ReactRendererIsActing/IsThisRendererActing

Sunil Pai committed Jul 2, 2019 at 22:20 UTC a457e02ae3a2d3903fcf8748380b1cc293a2445e
18 files changed +85 -75
fixtures/dom/src/index.test.js
+10 -8
@@ -70,10 +70,7 @@ it('warns when using the wrong act version - test + dom: updates', () => {
70 TestRenderer.act(() => {
71 setCtr(1);
72 });
73 - }).toWarnDev([
74 - 'An update to Counter inside a test was not wrapped in act',
75 - "It looks like you're using the wrong act()",
76 - ]);
73 + }).toWarnDev(["It looks like you're using the wrong act()"]);
74 });
75
76 it('warns when using the wrong act version - dom + test: .create()', () => {
@@ -109,10 +106,7 @@ it('warns when using the wrong act version - dom + test: updates', () => {
106 TestUtils.act(() => {
107 setCtr(1);
108 });
112 - }).toWarnDev([
113 - 'An update to Counter inside a test was not wrapped in act',
114 - "It looks like you're using the wrong act()",
115 - ]);
109 + }).toWarnDev(["It looks like you're using the wrong act()"]);
110 });
111
112 const {Surface, Group, Shape} = ReactART;
@@ -158,3 +152,11 @@ it('does not warn when nesting react-act inside react-test-renderer', () => {
152 TestRenderer.create(<ARTTest />);
153 });
154 });
155 +
156 +it("doesn't warn if you use nested acts from different renderers", () => {
157 + TestRenderer.act(() => {
158 + TestUtils.act(() => {
159 + TestRenderer.create(<App />);
160 + });
161 + });
162 +});
packages/react-art/src/ReactARTHostConfig.js
+1 -1
@@ -347,7 +347,7 @@ export function shouldSetTextContent(type, props) {
347 export const isPrimaryRenderer = false;
348
349 // The ART renderer shouldn't trigger missing act() warnings
350 -export const shouldWarnUnactedUpdates = false;
350 +export const warnsIfNotActing = false;
351
352 export const supportsMutation = true;
353
packages/react-dom/src/client/ReactDOM.js
+2 -2
@@ -38,7 +38,7 @@ import {
38 findHostInstance,
39 findHostInstanceWithWarning,
40 flushPassiveEffects,
41 - ReactActingRendererSigil,
41 + IsThisRendererActing,
42 } from 'react-reconciler/inline.dom';
43 import {createPortal as createPortalImpl} from 'shared/ReactPortal';
44 import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -817,7 +817,7 @@ const ReactDOM: Object = {
817 dispatchEvent,
818 runEventsInBatch,
819 flushPassiveEffects,
820 - ReactActingRendererSigil,
820 + IsThisRendererActing,
821 ],
822 },
823 };
packages/react-dom/src/client/ReactDOMHostConfig.js
+1 -1
@@ -348,7 +348,7 @@ export function createTextInstance(
348 }
349
350 export const isPrimaryRenderer = true;
351 -export const shouldWarnUnactedUpdates = true;
351 +export const warnsIfNotActing = true;
352 // This initialization code may run even on server environments
353 // if a component just imports ReactDOM (e.g. for findDOMNode).
354 // Some environments might not have setTimeout or clearTimeout.
packages/react-dom/src/fire/ReactFire.js
+2 -2
@@ -43,7 +43,7 @@ import {
43 findHostInstance,
44 findHostInstanceWithWarning,
45 flushPassiveEffects,
46 - ReactActingRendererSigil,
46 + IsThisRendererActing,
47 } from 'react-reconciler/inline.fire';
48 import {createPortal as createPortalImpl} from 'shared/ReactPortal';
49 import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -823,7 +823,7 @@ const ReactDOM: Object = {
823 dispatchEvent,
824 runEventsInBatch,
825 flushPassiveEffects,
826 - ReactActingRendererSigil,
826 + IsThisRendererActing,
827 ],
828 },
829 };
packages/react-dom/src/test-utils/ReactTestUtils.js
+1 -1
@@ -44,7 +44,7 @@ const [
44 runEventsInBatch,
45 /* eslint-disable no-unused-vars */
46 flushPassiveEffects,
47 - ReactActingRendererSigil,
47 + IsThisRendererActing,
48 /* eslint-enable no-unused-vars */
49 ] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
50
packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+10 -6
@@ -33,12 +33,12 @@ const [
33 runEventsInBatch,
34 /* eslint-enable no-unused-vars */
35 flushPassiveEffects,
36 - ReactActingRendererSigil,
36 + IsThisRendererActing,
37 ] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
38
39 const batchedUpdates = ReactDOM.unstable_batchedUpdates;
40
41 -const {ReactCurrentActingRendererSigil} = ReactSharedInternals;
41 +const {IsSomeRendererActing} = ReactSharedInternals;
42
43 // this implementation should be exactly the same in
44 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
@@ -86,17 +86,21 @@ let actingUpdatesScopeDepth = 0;
86
87 function act(callback: () => Thenable) {
88 let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
89 - let previousActingUpdatesSigil;
89 + let previousIsSomeRendererActing;
90 + let previousIsThisRendererActing;
91 actingUpdatesScopeDepth++;
92 if (__DEV__) {
92 - previousActingUpdatesSigil = ReactCurrentActingRendererSigil.current;
93 - ReactCurrentActingRendererSigil.current = ReactActingRendererSigil;
93 + previousIsSomeRendererActing = IsSomeRendererActing.current;
94 + previousIsThisRendererActing = IsSomeRendererActing.current;
95 + IsSomeRendererActing.current = true;
96 + IsThisRendererActing.current = true;
97 }
98
99 function onDone() {
100 actingUpdatesScopeDepth--;
101 if (__DEV__) {
99 - ReactCurrentActingRendererSigil.current = previousActingUpdatesSigil;
102 + IsSomeRendererActing.current = previousIsSomeRendererActing;
103 + IsThisRendererActing.current = previousIsThisRendererActing;
104 if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
105 // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
106 warningWithoutStack(
packages/react-native-renderer/src/ReactFabricHostConfig.js
+1 -1
@@ -352,7 +352,7 @@ export function shouldSetTextContent(type: string, props: Props): boolean {
352 export const isPrimaryRenderer = false;
353
354 // The Fabric renderer shouldn't trigger missing act() warnings
355 -export const shouldWarnUnactedUpdates = false;
355 +export const warnsIfNotActing = false;
356
357 export const scheduleTimeout = setTimeout;
358 export const cancelTimeout = clearTimeout;
packages/react-native-renderer/src/ReactNativeHostConfig.js
+1 -1
@@ -247,7 +247,7 @@ export function resetAfterCommit(containerInfo: Container): void {
247 }
248
249 export const isPrimaryRenderer = true;
250 -export const shouldWarnUnactedUpdates = true;
250 +export const warnsIfNotActing = true;
251
252 export const scheduleTimeout = setTimeout;
253 export const cancelTimeout = clearTimeout;
packages/react-noop-renderer/src/createReactNoop.js
+11 -7
@@ -65,7 +65,7 @@ type TextInstance = {|
65 |};
66 type HostContext = Object;
67
68 -const {ReactCurrentActingRendererSigil} = ReactSharedInternals;
68 +const {IsSomeRendererActing} = ReactSharedInternals;
69
70 const NO_CONTEXT = {};
71 const UPPERCASE_CONTEXT = {};
@@ -393,7 +393,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
393 now: Scheduler.unstable_now,
394
395 isPrimaryRenderer: true,
396 - shouldWarnUnactedUpdates: true,
396 + warnsIfNotActing: true,
397 supportsHydration: false,
398
399 mountEventComponent(): void {
@@ -566,7 +566,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
566 const {
567 flushPassiveEffects,
568 batchedUpdates,
569 - ReactActingRendererSigil,
569 + IsThisRendererActing,
570 } = NoopRenderer;
571
572 // this act() implementation should be exactly the same in
@@ -615,17 +615,21 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
615
616 function act(callback: () => Thenable) {
617 let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
618 - let previousActingUpdatesSigil;
618 + let previousIsSomeRendererActing;
619 + let previousIsThisRendererActing;
620 actingUpdatesScopeDepth++;
621 if (__DEV__) {
621 - previousActingUpdatesSigil = ReactCurrentActingRendererSigil.current;
622 - ReactCurrentActingRendererSigil.current = ReactActingRendererSigil;
622 + previousIsSomeRendererActing = IsSomeRendererActing.current;
623 + previousIsThisRendererActing = IsSomeRendererActing.current;
624 + IsSomeRendererActing.current = true;
625 + IsThisRendererActing.current = true;
626 }
627
628 function onDone() {
629 actingUpdatesScopeDepth--;
630 if (__DEV__) {
628 - ReactCurrentActingRendererSigil.current = previousActingUpdatesSigil;
631 + IsSomeRendererActing.current = previousIsSomeRendererActing;
632 + IsThisRendererActing.current = previousIsThisRendererActing;
633 if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
634 // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
635 warningWithoutStack(
packages/react-reconciler/src/ReactFiberReconciler.js
+2 -2
@@ -57,7 +57,7 @@ import {
57 flushDiscreteUpdates,
58 flushPassiveEffects,
59 warnIfNotScopedWithMatchingAct,
60 - ReactActingRendererSigil,
60 + IsThisRendererActing,
61 } from './ReactFiberWorkLoop';
62 import {createUpdate, enqueueUpdate} from './ReactUpdateQueue';
63 import ReactFiberInstrumentation from './ReactFiberInstrumentation';
@@ -345,7 +345,7 @@ export {
345 flushControlled,
346 flushSync,
347 flushPassiveEffects,
348 - ReactActingRendererSigil,
348 + IsThisRendererActing,
349 };
350
351 export function getPublicRootInstance(
packages/react-reconciler/src/ReactFiberWorkLoop.js
+12 -14
@@ -55,7 +55,7 @@ import {
55 scheduleTimeout,
56 cancelTimeout,
57 noTimeout,
58 - shouldWarnUnactedUpdates,
58 + warnsIfNotActing,
59 } from './ReactFiberHostConfig';
60
61 import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber';
@@ -174,7 +174,7 @@ const ceil = Math.ceil;
174 const {
175 ReactCurrentDispatcher,
176 ReactCurrentOwner,
177 - ReactCurrentActingRendererSigil,
177 + IsSomeRendererActing,
178 } = ReactSharedInternals;
179
180 type ExecutionContext = number;
@@ -2420,18 +2420,14 @@ function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) {
2420 }
2421 }
2422
2423 -// We export a simple object here to be used by a renderer/test-utils
2424 -// as the value of ReactCurrentActingRendererSigil.current
2425 -// This identity lets us identify (ha!) when the wrong renderer's act()
2426 -// wraps anothers' updates/effects
2427 -export const ReactActingRendererSigil = {};
2423 +export const IsThisRendererActing = {current: (false: boolean)};
2424
2425 export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
2426 if (__DEV__) {
2427 if (
2432 - shouldWarnUnactedUpdates === true &&
2433 - ReactCurrentActingRendererSigil.current !== null &&
2434 - ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
2428 + warnsIfNotActing === true &&
2429 + IsSomeRendererActing.current === true &&
2430 + IsThisRendererActing.current !== true
2431 ) {
2432 warningWithoutStack(
2433 false,
@@ -2456,8 +2452,9 @@ export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
2452 export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
2453 if (__DEV__) {
2454 if (
2459 - shouldWarnUnactedUpdates === true &&
2460 - ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
2455 + warnsIfNotActing === true &&
2456 + IsSomeRendererActing.current === false &&
2457 + IsThisRendererActing.current === false
2458 ) {
2459 warningWithoutStack(
2460 false,
@@ -2482,9 +2479,10 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
2479 function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2480 if (__DEV__) {
2481 if (
2485 - shouldWarnUnactedUpdates === true &&
2482 + warnsIfNotActing === true &&
2483 executionContext === NoContext &&
2487 - ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
2484 + IsSomeRendererActing.current === false &&
2485 + IsThisRendererActing.current === false
2486 ) {
2487 warningWithoutStack(
2488 false,
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+1 -1
@@ -59,7 +59,7 @@ export const cancelTimeout = $$$hostConfig.clearTimeout;
59 export const noTimeout = $$$hostConfig.noTimeout;
60 export const now = $$$hostConfig.now;
61 export const isPrimaryRenderer = $$$hostConfig.isPrimaryRenderer;
62 -export const shouldWarnUnactedUpdates = $$$hostConfig.shouldWarnUnactedUpdates;
62 +export const warnsIfNotActing = $$$hostConfig.warnsIfNotActing;
63 export const supportsMutation = $$$hostConfig.supportsMutation;
64 export const supportsPersistence = $$$hostConfig.supportsPersistence;
65 export const supportsHydration = $$$hostConfig.supportsHydration;
packages/react-test-renderer/src/ReactTestHostConfig.js
+1 -1
@@ -217,7 +217,7 @@ export function createTextInstance(
217 }
218
219 export const isPrimaryRenderer = false;
220 -export const shouldWarnUnactedUpdates = true;
220 +export const warnsIfNotActing = true;
221
222 export const scheduleTimeout = setTimeout;
223 export const cancelTimeout = clearTimeout;
packages/react-test-renderer/src/ReactTestRendererAct.js
+10 -6
@@ -11,7 +11,7 @@ import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop';
11 import {
12 batchedUpdates,
13 flushPassiveEffects,
14 - ReactActingRendererSigil,
14 + IsThisRendererActing,
15 } from 'react-reconciler/inline.test';
16 import ReactSharedInternals from 'shared/ReactSharedInternals';
17 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -19,7 +19,7 @@ import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
19 import enqueueTask from 'shared/enqueueTask';
20 import * as Scheduler from 'scheduler';
21
22 -const {ReactCurrentActingRendererSigil} = ReactSharedInternals;
22 +const {IsSomeRendererActing} = ReactSharedInternals;
23
24 // this implementation should be exactly the same in
25 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
@@ -67,17 +67,21 @@ let actingUpdatesScopeDepth = 0;
67
68 function act(callback: () => Thenable) {
69 let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
70 - let previousActingUpdatesSigil;
70 + let previousIsSomeRendererActing;
71 + let previousIsThisRendererActing;
72 actingUpdatesScopeDepth++;
73 if (__DEV__) {
73 - previousActingUpdatesSigil = ReactCurrentActingRendererSigil.current;
74 - ReactCurrentActingRendererSigil.current = ReactActingRendererSigil;
74 + previousIsSomeRendererActing = IsSomeRendererActing.current;
75 + previousIsThisRendererActing = IsSomeRendererActing.current;
76 + IsSomeRendererActing.current = true;
77 + IsThisRendererActing.current = true;
78 }
79
80 function onDone() {
81 actingUpdatesScopeDepth--;
82 if (__DEV__) {
80 - ReactCurrentActingRendererSigil.current = previousActingUpdatesSigil;
83 + IsSomeRendererActing.current = previousIsSomeRendererActing;
84 + IsThisRendererActing.current = previousIsThisRendererActing;
85 if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
86 // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
87 warningWithoutStack(
packages/react/src/IsSomeRendererActing.js new
+17
@@ -0,0 +1,17 @@
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 +/**
11 + * Used by act() to track whether you're inside an act() scope.
12 + */
13 +
14 +const IsSomeRendererActing = {
15 + current: (false: boolean),
16 +};
17 +export default IsSomeRendererActing;
packages/react/src/ReactCurrentActingRendererSigil.js deleted
-19
@@ -1,19 +0,0 @@
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 -/**
11 - * Used by act() to track whether you're outside an act() scope.
12 - * We use a renderer's flushPassiveEffects as the sigil value
13 - * so we can track identity of the renderer.
14 - */
15 -
16 -const ReactCurrentActingRendererSigil = {
17 - current: (null: null | (() => boolean)),
18 -};
19 -export default ReactCurrentActingRendererSigil;
packages/react/src/ReactSharedInternals.js
+2 -2
@@ -10,13 +10,13 @@ import ReactCurrentDispatcher from './ReactCurrentDispatcher';
10 import ReactCurrentBatchConfig from './ReactCurrentBatchConfig';
11 import ReactCurrentOwner from './ReactCurrentOwner';
12 import ReactDebugCurrentFrame from './ReactDebugCurrentFrame';
13 -import ReactCurrentActingRendererSigil from './ReactCurrentActingRendererSigil';
13 +import IsSomeRendererActing from './IsSomeRendererActing';
14
15 const ReactSharedInternals = {
16 ReactCurrentDispatcher,
17 ReactCurrentBatchConfig,
18 ReactCurrentOwner,
19 - ReactCurrentActingRendererSigil,
19 + IsSomeRendererActing,
20 // Used by renderers to avoid bundling object-assign twice in UMD bundles:
21 assign,
22 };