From 5b186a693c280363cc0126938602c00ccba71bad Mon Sep 17 00:00:00 2001
From: Sunil Pai <threepointone@gmail.com>
Date: Tue, 28 May 2019 23:38:30 +0100
Subject: [PATCH 1/6] warn when using the wrong renderer's act around another
 renderer's updates

like it says. it uses a real object as the sigil (instead of just a boolean). specifically, it uses a renderer's flushPassiveEffects as the sigil. We also run tests for this separate from our main suite (which doesn't allow loading multiple renderers in a suite), but makes sure to run this in CI as well.
---
 fixtures/dom/package.json                     |   2 +-
 fixtures/dom/public/act-dom.html              |  65 ++++++-----
 fixtures/dom/src/index.test.js                | 107 ++++++++++++++++++
 package.json                                  |   1 +
 .../src/test-utils/ReactTestUtilsAct.js       |  13 +--
 .../src/createReactNoop.js                    |  13 +--
 .../react-reconciler/src/ReactFiberHooks.js   |   2 +
 .../src/ReactFiberReconciler.js               |   7 ++
 .../src/ReactFiberWorkLoop.js                 |  40 ++++++-
 .../src/ReactTestRendererAct.js               |  13 +--
 .../react/src/ReactActingRendererSigil.js     |  19 ++++
 packages/react/src/ReactSharedInternals.js    |   4 +-
 12 files changed, 227 insertions(+), 59 deletions(-)
 create mode 100644 fixtures/dom/src/index.test.js
 create mode 100644 packages/react/src/ReactActingRendererSigil.js

diff --git a/fixtures/dom/package.json b/fixtures/dom/package.json
index 3282c85fa58a7..9bf118f0c29cd 100644
--- a/fixtures/dom/package.json
+++ b/fixtures/dom/package.json
@@ -18,7 +18,7 @@
   },
   "scripts": {
     "start": "react-scripts start",
-    "prestart": "cp ../../build/node_modules/scheduler/umd/scheduler-unstable_mock.development.js ../../build/node_modules/scheduler/umd/scheduler-unstable_mock.production.min.js ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.development.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.production.min.js public/",
+    "prestart": "cp ../../build/node_modules/scheduler/umd/scheduler-unstable_mock.development.js ../../build/node_modules/scheduler/umd/scheduler-unstable_mock.production.min.js ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.development.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.production.min.js public/ && cp -a ../../build/node_modules/. node_modules",
     "build": "react-scripts build && cp build/index.html build/200.html",
     "test": "react-scripts test --env=jsdom",
     "eject": "react-scripts eject"
diff --git a/fixtures/dom/public/act-dom.html b/fixtures/dom/public/act-dom.html
index 2100026a5ba45..2aa33da465e26 100644
--- a/fixtures/dom/public/act-dom.html
+++ b/fixtures/dom/public/act-dom.html
@@ -1,21 +1,21 @@
 <!DOCTYPE html>
 <html>
-<head>
-  <title>sanity test for ReactTestUtils.act</title>
-</head>
-<body>
-  this page tests whether act runs properly in a browser.
-  <br/>
-  your console should say "5"
-  <script src='scheduler-unstable_mock.development.js'></script>
-  <script src='react.development.js'></script>
-  <script type="text/javascript">    
-    window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler = window.SchedulerMock
-  </script>
-  <script src='react-dom.development.js'></script>
-  <script src='react-dom-test-utils.development.js'></script>
-  <script>
-    async function run(){
+  <head>
+    <title>sanity test for ReactTestUtils.act</title>
+  </head>
+  <body>
+    this page tests whether act runs properly in a browser.
+    <br />
+    your console should say "5"
+    <script src="scheduler-unstable_mock.development.js"></script>
+    <script src="react.development.js"></script>
+    <script type="text/javascript">
+      window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Scheduler =
+        window.SchedulerMock;
+    </script>
+    <script src="react-dom.development.js"></script>
+    <script src="react-dom-test-utils.development.js"></script>
+    <script>
       // from ReactTestUtilsAct-test.js
       function App() {
         let [state, setState] = React.useState(0);
@@ -23,23 +23,22 @@
           await null;
           setState(x => x + 1);
         }
-        React.useEffect(
-          () => {
-            ticker();
-          },
-          [Math.min(state, 4)],
-        );
+        React.useEffect(() => {
+          ticker();
+        }, [Math.min(state, 4)]);
         return state;
       }
-      const el = document.createElement('div');
-      await ReactTestUtils.act(async () => {
-        ReactDOM.render(React.createElement(App), el);
-      });
-      // all 5 ticks present and accounted for
-      console.log(el.innerHTML);
-    }
-    run();
-    
-  </script>
-</body>
+
+      async function testAsyncAct() {
+        const el = document.createElement("div");
+        await ReactTestUtils.act(async () => {
+          ReactDOM.render(React.createElement(App), el);
+        });
+        // all 5 ticks present and accounted for
+        console.log(el.innerHTML);
+      }
+
+      testAsyncAct();
+    </script>
+  </body>
 </html>
diff --git a/fixtures/dom/src/index.test.js b/fixtures/dom/src/index.test.js
new file mode 100644
index 0000000000000..0eddb918706b3
--- /dev/null
+++ b/fixtures/dom/src/index.test.js
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import TestUtils from 'react-dom/test-utils';
+import TestRenderer from 'react-test-renderer';
+
+let spy;
+beforeEach(() => {
+  spy = jest.spyOn(console, 'error').mockImplementation(() => {});
+});
+
+function confirmWarning() {
+  expect(spy).toHaveBeenCalledWith(
+    expect.stringContaining(
+      "It looks like you're using the wrong act() around your test interactions."
+    ),
+    ''
+  );
+}
+
+function App(props) {
+  return 'hello world';
+}
+
+it("doesn't warn when you use the right act + renderer: dom", () => {
+  TestUtils.act(() => {
+    TestUtils.renderIntoDocument(<App />);
+  });
+  expect(spy).not.toHaveBeenCalled();
+});
+
+it("doesn't warn when you use the right act + renderer: test", () => {
+  TestRenderer.act(() => {
+    TestRenderer.create(<App />);
+  });
+  expect(spy).not.toHaveBeenCalled();
+});
+
+it('works with createRoot().render combo', () => {
+  const root = ReactDOM.unstable_createRoot(document.createElement('div'));
+  TestRenderer.act(() => {
+    root.render(<App />);
+  });
+  confirmWarning();
+});
+
+it('warns when using the wrong act version - test + dom: render', () => {
+  TestRenderer.act(() => {
+    TestUtils.renderIntoDocument(<App />);
+  });
+  confirmWarning();
+});
+
+it('warns when using the wrong act version - test + dom: updates', () => {
+  let setCtr;
+  function Counter(props) {
+    const [ctr, _setCtr] = React.useState(0);
+    setCtr = _setCtr;
+    return ctr;
+  }
+  TestUtils.renderIntoDocument(<Counter />);
+  TestRenderer.act(() => {
+    setCtr(1);
+  });
+  confirmWarning();
+});
+
+it('warns when using the wrong act version - dom + test: .create()', () => {
+  TestUtils.act(() => {
+    TestRenderer.create(<App />);
+  });
+  confirmWarning();
+});
+
+it('warns when using the wrong act version - dom + test: .update()', () => {
+  let root;
+  // use the right one here so we don't get the first warning
+  TestRenderer.act(() => {
+    root = TestRenderer.create(<App key="one" />);
+  });
+  TestUtils.act(() => {
+    root.update(<App key="two" />);
+  });
+  confirmWarning();
+});
+
+it('warns when using the wrong act version - dom + test: updates', () => {
+  let setCtr;
+  function Counter(props) {
+    const [ctr, _setCtr] = React.useState(0);
+    setCtr = _setCtr;
+    return ctr;
+  }
+  const root = TestRenderer.create(<Counter />);
+  TestUtils.act(() => {
+    setCtr(1);
+  });
+  confirmWarning();
+});
diff --git a/package.json b/package.json
index 97998b6b6d4f7..03ba206a0d3bd 100644
--- a/package.json
+++ b/package.json
@@ -107,6 +107,7 @@
     "test-prod-build": "yarn test-build-prod",
     "test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js",
     "test-build-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.build.js",
+    "test-dom-fixture": "cd fixtures/dom && yarn && yarn prestart && yarn test",
     "flow": "node ./scripts/tasks/flow.js",
     "flow-ci": "node ./scripts/tasks/flow-ci.js",
     "prettier": "node ./scripts/prettier/index.js write-changed",
diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
index bf4472b009745..b0e0ebe80fd1e 100644
--- a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+++ b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
@@ -37,7 +37,7 @@ const [
 
 const batchedUpdates = ReactDOM.unstable_batchedUpdates;
 
-const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
+const {ReactActingRendererSigil} = ReactSharedInternals;
 
 // this implementation should be exactly the same in
 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
@@ -85,17 +85,16 @@ let actingUpdatesScopeDepth = 0;
 
 function act(callback: () => Thenable) {
   let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
+  let previousActingUpdatesSigil = ReactActingRendererSigil.current;
   actingUpdatesScopeDepth++;
-  if (__DEV__) {
-    ReactShouldWarnActingUpdates.current = true;
-  }
+  // we use the function flushPassiveEffects directly as the sigil,
+  // since it's unique to a renderer
+  ReactActingRendererSigil.current = flushPassiveEffects;
 
   function onDone() {
     actingUpdatesScopeDepth--;
+    ReactActingRendererSigil.current = previousActingUpdatesSigil;
     if (__DEV__) {
-      if (actingUpdatesScopeDepth === 0) {
-        ReactShouldWarnActingUpdates.current = false;
-      }
       if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
         // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
         warningWithoutStack(
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index a21a9d95c1bab..60a702a0b9eb4 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -81,7 +81,7 @@ type TextInstance = {|
 |};
 type HostContext = Object;
 
-const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
+const {ReactActingRendererSigil} = ReactSharedInternals;
 
 const NO_CONTEXT = {};
 const UPPERCASE_CONTEXT = {};
@@ -698,17 +698,16 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
 
   function act(callback: () => Thenable) {
     let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
+    let previousActingUpdatesSigil = ReactActingRendererSigil.current;
     actingUpdatesScopeDepth++;
-    if (__DEV__) {
-      ReactShouldWarnActingUpdates.current = true;
-    }
+    // we use the function flushPassiveEffects directly as the sigil,
+    // since it's unique to a renderer
+    ReactActingRendererSigil.current = flushPassiveEffects;
 
     function onDone() {
       actingUpdatesScopeDepth--;
+      ReactActingRendererSigil.current = previousActingUpdatesSigil;
       if (__DEV__) {
-        if (actingUpdatesScopeDepth === 0) {
-          ReactShouldWarnActingUpdates.current = false;
-        }
         if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
           // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
           warningWithoutStack(
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index c3e9acdaa611f..aa3b10ed7463a 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -35,6 +35,7 @@ import {
   flushPassiveEffects,
   requestCurrentTime,
   warnIfNotCurrentlyActingUpdatesInDev,
+  warnIfNotScopedWithMatchingAct,
   markRenderEventTimeAndConfig,
 } from './ReactFiberWorkLoop';
 
@@ -1211,6 +1212,7 @@ function dispatchAction<S, A>(
       // further, this isn't a test file, so flow doesn't recognize the symbol. So...
       // $FlowExpectedError - because requirements don't give a damn about your type sigs.
       if ('undefined' !== typeof jest) {
+        warnIfNotScopedWithMatchingAct(fiber);
         warnIfNotCurrentlyActingUpdatesInDev(fiber);
       }
     }
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index 16214aa0332f5..fd313a8e48632 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -56,6 +56,7 @@ import {
   discreteUpdates,
   flushDiscreteUpdates,
   flushPassiveEffects,
+  warnIfNotScopedWithMatchingAct,
 } from './ReactFiberWorkLoop';
 import {createUpdate, enqueueUpdate} from './ReactUpdateQueue';
 import ReactFiberInstrumentation from './ReactFiberInstrumentation';
@@ -303,6 +304,12 @@ export function updateContainer(
 ): ExpirationTime {
   const current = container.current;
   const currentTime = requestCurrentTime();
+  if (__DEV__) {
+    // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
+    if ('undefined' !== typeof jest) {
+      warnIfNotScopedWithMatchingAct(current);
+    }
+  }
   const suspenseConfig = requestCurrentSuspenseConfig();
   const expirationTime = computeExpirationForFiber(
     currentTime,
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index c93985e536cea..75582654f5a9e 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -173,7 +173,7 @@ const ceil = Math.ceil;
 const {
   ReactCurrentDispatcher,
   ReactCurrentOwner,
-  ReactShouldWarnActingUpdates,
+  ReactActingRendererSigil,
 } = ReactSharedInternals;
 
 type WorkPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6;
@@ -2276,11 +2276,47 @@ function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) {
   }
 }
 
+export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
+  if (__DEV__) {
+    if (
+      ReactActingRendererSigil.current !== null &&
+      // use the function flushPassiveEffects directly as the sigil
+      // so this comparison is expected here
+      ReactActingRendererSigil.current !== flushPassiveEffects
+    ) {
+      // it looks like we're using the wrong matching act(), so log a warning
+      warningWithoutStack(
+        false,
+        "It looks like you're using the wrong act() around your test interactions.\n" +
+          'Be sure to use the matching version of act() corresponding to your renderer:\n\n' +
+          '// for react-dom:\n' +
+          "import {act} from 'react-test-utils';\n" +
+          '//...\n' +
+          'act(() => ...);\n\n' +
+          '// for react-test-renderer:\n' +
+          "import TestRenderer from 'react-test-renderer';\n" +
+          'const {act} = TestRenderer;\n' +
+          '//...\n' +
+          'act(() => ...);' +
+          '%s',
+        getStackByFiberInDevAndProd(fiber),
+      );
+    }
+  }
+}
+
+// in a test-like environment, we want to warn if dispatchAction() is
+// called outside of a TestUtils.act(...)/batchedUpdates/render call.
+// so we have a a step counter for when we descend/ascend from
+// act() calls, and test on it for when to warn
+// It's a tuple with a single value. Look into ReactTestUtilsAct as an
+// example of how we change the value
+
 function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
   if (__DEV__) {
     if (
       workPhase === NotWorking &&
-      ReactShouldWarnActingUpdates.current === false
+      ReactActingRendererSigil.current !== flushPassiveEffects
     ) {
       warningWithoutStack(
         false,
diff --git a/packages/react-test-renderer/src/ReactTestRendererAct.js b/packages/react-test-renderer/src/ReactTestRendererAct.js
index eafcae3ba79d2..98eee873317c0 100644
--- a/packages/react-test-renderer/src/ReactTestRendererAct.js
+++ b/packages/react-test-renderer/src/ReactTestRendererAct.js
@@ -18,7 +18,7 @@ import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
 import enqueueTask from 'shared/enqueueTask';
 import * as Scheduler from 'scheduler';
 
-const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
+const {ReactActingRendererSigil} = ReactSharedInternals;
 
 // this implementation should be exactly the same in
 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
@@ -66,17 +66,16 @@ let actingUpdatesScopeDepth = 0;
 
 function act(callback: () => Thenable) {
   let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
+  let previousActingUpdatesSigil = ReactActingRendererSigil.current;
   actingUpdatesScopeDepth++;
-  if (__DEV__) {
-    ReactShouldWarnActingUpdates.current = true;
-  }
+  // we use the function flushPassiveEffects directly as the sigil,
+  // since it's unique to a renderer
+  ReactActingRendererSigil.current = flushPassiveEffects;
 
   function onDone() {
     actingUpdatesScopeDepth--;
+    ReactActingRendererSigil.current = previousActingUpdatesSigil;
     if (__DEV__) {
-      if (actingUpdatesScopeDepth === 0) {
-        ReactShouldWarnActingUpdates.current = false;
-      }
       if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
         // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
         warningWithoutStack(
diff --git a/packages/react/src/ReactActingRendererSigil.js b/packages/react/src/ReactActingRendererSigil.js
new file mode 100644
index 0000000000000..a40ecca4c6604
--- /dev/null
+++ b/packages/react/src/ReactActingRendererSigil.js
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+/**
+ * Used by act() to track whether you're outside an act() scope.
+ * We use a renderer's flushPassiveEffects as the sigil value
+ * so we can track identity of the renderer.
+ */
+
+const ReactActingRendererSigil = {
+  current: (null: null | (() => boolean)),
+};
+export default ReactActingRendererSigil;
diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternals.js
index 1bc95cffaad4e..de4a14adb3dbf 100644
--- a/packages/react/src/ReactSharedInternals.js
+++ b/packages/react/src/ReactSharedInternals.js
@@ -10,13 +10,13 @@ import ReactCurrentDispatcher from './ReactCurrentDispatcher';
 import ReactCurrentBatchConfig from './ReactCurrentBatchConfig';
 import ReactCurrentOwner from './ReactCurrentOwner';
 import ReactDebugCurrentFrame from './ReactDebugCurrentFrame';
+import ReactActingRendererSigil from './ReactActingRendererSigil';
 
 const ReactSharedInternals = {
   ReactCurrentDispatcher,
   ReactCurrentBatchConfig,
   ReactCurrentOwner,
-  // used by act()
-  ReactShouldWarnActingUpdates: {current: false},
+  ReactActingRendererSigil,
   // Used by renderers to avoid bundling object-assign twice in UMD bundles:
   assign,
 };

From e789900ae1e0557e394f9d7d0154b54c47f3c1a1 Mon Sep 17 00:00:00 2001
From: Sunil Pai <threepointone@gmail.com>
Date: Tue, 28 May 2019 23:41:52 +0100
Subject: [PATCH 2/6] unneeded (and wrong) comment

---
 packages/react-reconciler/src/ReactFiberWorkLoop.js | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index 75582654f5a9e..a0edd5a1b8a4e 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -2305,13 +2305,6 @@ export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
   }
 }
 
-// in a test-like environment, we want to warn if dispatchAction() is
-// called outside of a TestUtils.act(...)/batchedUpdates/render call.
-// so we have a a step counter for when we descend/ascend from
-// act() calls, and test on it for when to warn
-// It's a tuple with a single value. Look into ReactTestUtilsAct as an
-// example of how we change the value
-
 function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
   if (__DEV__) {
     if (

From a2dae63111014ac26910196fa332dcb8d19d7d3f Mon Sep 17 00:00:00 2001
From: Sunil Pai <threepointone@gmail.com>
Date: Wed, 29 May 2019 00:00:07 +0100
Subject: [PATCH 3/6] run the dom fixture on CI

---
 .circleci/config.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 75f249e839a0b..a390575b20289 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -164,6 +164,7 @@ jobs:
       - *restore_yarn_cache
       - *run_yarn
       - run: yarn test-build --maxWorkers=2
+      - run: yarn test-dom-fixture
 
   test_fuzz:
     docker: *docker

From 6d6419443e9a91e385437c183a659b17633bba0c Mon Sep 17 00:00:00 2001
From: Sunil Pai <threepointone@gmail.com>
Date: Wed, 29 May 2019 12:16:36 +0100
Subject: [PATCH 4/6] update the sigil only in __DEV__

---
 packages/react-dom/src/test-utils/ReactTestUtilsAct.js   | 9 ++++++---
 packages/react-noop-renderer/src/createReactNoop.js      | 9 ++++++---
 packages/react-test-renderer/src/ReactTestRendererAct.js | 9 ++++++---
 3 files changed, 18 insertions(+), 9 deletions(-)

diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
index b0e0ebe80fd1e..f1fe7646626a7 100644
--- a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+++ b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
@@ -85,16 +85,19 @@ let actingUpdatesScopeDepth = 0;
 
 function act(callback: () => Thenable) {
   let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
-  let previousActingUpdatesSigil = ReactActingRendererSigil.current;
+  let previousActingUpdatesSigil;
   actingUpdatesScopeDepth++;
   // we use the function flushPassiveEffects directly as the sigil,
   // since it's unique to a renderer
-  ReactActingRendererSigil.current = flushPassiveEffects;
+  if (__DEV__) {
+    previousActingUpdatesSigil = ReactActingRendererSigil.current;
+    ReactActingRendererSigil.current = flushPassiveEffects;
+  }
 
   function onDone() {
     actingUpdatesScopeDepth--;
-    ReactActingRendererSigil.current = previousActingUpdatesSigil;
     if (__DEV__) {
+      ReactActingRendererSigil.current = previousActingUpdatesSigil;
       if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
         // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
         warningWithoutStack(
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index 60a702a0b9eb4..3ca5aaa545765 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -698,16 +698,19 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
 
   function act(callback: () => Thenable) {
     let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
-    let previousActingUpdatesSigil = ReactActingRendererSigil.current;
+    let previousActingUpdatesSigil;
     actingUpdatesScopeDepth++;
     // we use the function flushPassiveEffects directly as the sigil,
     // since it's unique to a renderer
-    ReactActingRendererSigil.current = flushPassiveEffects;
+    if (__DEV__) {
+      previousActingUpdatesSigil = ReactActingRendererSigil.current;
+      ReactActingRendererSigil.current = flushPassiveEffects;
+    }
 
     function onDone() {
       actingUpdatesScopeDepth--;
-      ReactActingRendererSigil.current = previousActingUpdatesSigil;
       if (__DEV__) {
+        ReactActingRendererSigil.current = previousActingUpdatesSigil;
         if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
           // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
           warningWithoutStack(
diff --git a/packages/react-test-renderer/src/ReactTestRendererAct.js b/packages/react-test-renderer/src/ReactTestRendererAct.js
index 98eee873317c0..39087f111028e 100644
--- a/packages/react-test-renderer/src/ReactTestRendererAct.js
+++ b/packages/react-test-renderer/src/ReactTestRendererAct.js
@@ -66,16 +66,19 @@ let actingUpdatesScopeDepth = 0;
 
 function act(callback: () => Thenable) {
   let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
-  let previousActingUpdatesSigil = ReactActingRendererSigil.current;
+  let previousActingUpdatesSigil;
   actingUpdatesScopeDepth++;
   // we use the function flushPassiveEffects directly as the sigil,
   // since it's unique to a renderer
-  ReactActingRendererSigil.current = flushPassiveEffects;
+  if (__DEV__) {
+    previousActingUpdatesSigil = ReactActingRendererSigil.current;
+    ReactActingRendererSigil.current = flushPassiveEffects;
+  }
 
   function onDone() {
     actingUpdatesScopeDepth--;
-    ReactActingRendererSigil.current = previousActingUpdatesSigil;
     if (__DEV__) {
+      ReactActingRendererSigil.current = previousActingUpdatesSigil;
       if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
         // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
         warningWithoutStack(

From 669d92e2aa128af986abe2304c5aaad643b25bdf Mon Sep 17 00:00:00 2001
From: Sunil Pai <threepointone@gmail.com>
Date: Wed, 29 May 2019 12:20:27 +0100
Subject: [PATCH 5/6] remove the obnoxious comment

---
 packages/react-reconciler/src/ReactFiberHooks.js | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index aa3b10ed7463a..2946c931cdc58 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -1208,9 +1208,7 @@ function dispatchAction<S, A>(
       }
     }
     if (__DEV__) {
-      // jest isn't a 'global', it's just exposed to tests via a wrapped function
-      // further, this isn't a test file, so flow doesn't recognize the symbol. So...
-      // $FlowExpectedError - because requirements don't give a damn about your type sigs.
+      // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
       if ('undefined' !== typeof jest) {
         warnIfNotScopedWithMatchingAct(fiber);
         warnIfNotCurrentlyActingUpdatesInDev(fiber);

From c0dc3989e8c40595610d684636f9b32e1c5d0754 Mon Sep 17 00:00:00 2001
From: Sunil Pai <threepointone@gmail.com>
Date: Wed, 29 May 2019 20:27:30 +0100
Subject: [PATCH 6/6] use an explicit export for the sigil

---
 packages/react-dom/src/client/ReactDOM.js        |  2 ++
 packages/react-dom/src/fire/ReactFire.js         |  2 ++
 .../react-dom/src/test-utils/ReactTestUtils.js   |  4 +++-
 .../src/test-utils/ReactTestUtilsAct.js          | 11 +++++------
 .../react-noop-renderer/src/createReactNoop.js   | 16 +++++++++-------
 .../react-reconciler/src/ReactFiberReconciler.js |  2 ++
 .../react-reconciler/src/ReactFiberWorkLoop.js   | 14 ++++++++++----
 .../src/ReactTestRendererAct.js                  | 11 +++++------
 ...gil.js => ReactCurrentActingRendererSigil.js} |  4 ++--
 packages/react/src/ReactSharedInternals.js       |  4 ++--
 10 files changed, 42 insertions(+), 28 deletions(-)
 rename packages/react/src/{ReactActingRendererSigil.js => ReactCurrentActingRendererSigil.js} (82%)

diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js
index 3629ed3e1d6aa..1db9e527efe19 100644
--- a/packages/react-dom/src/client/ReactDOM.js
+++ b/packages/react-dom/src/client/ReactDOM.js
@@ -38,6 +38,7 @@ import {
   findHostInstance,
   findHostInstanceWithWarning,
   flushPassiveEffects,
+  ReactActingRendererSigil,
 } from 'react-reconciler/inline.dom';
 import {createPortal as createPortalImpl} from 'shared/ReactPortal';
 import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -816,6 +817,7 @@ const ReactDOM: Object = {
       dispatchEvent,
       runEventsInBatch,
       flushPassiveEffects,
+      ReactActingRendererSigil,
     ],
   },
 };
diff --git a/packages/react-dom/src/fire/ReactFire.js b/packages/react-dom/src/fire/ReactFire.js
index 6b22b7c43d678..bb74931fdc3ef 100644
--- a/packages/react-dom/src/fire/ReactFire.js
+++ b/packages/react-dom/src/fire/ReactFire.js
@@ -43,6 +43,7 @@ import {
   findHostInstance,
   findHostInstanceWithWarning,
   flushPassiveEffects,
+  ReactActingRendererSigil,
 } from 'react-reconciler/inline.fire';
 import {createPortal as createPortalImpl} from 'shared/ReactPortal';
 import {canUseDOM} from 'shared/ExecutionEnvironment';
@@ -822,6 +823,7 @@ const ReactDOM: Object = {
       dispatchEvent,
       runEventsInBatch,
       flushPassiveEffects,
+      ReactActingRendererSigil,
     ],
   },
 };
diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js
index 5e39f754a2523..5f4d96fa9632b 100644
--- a/packages/react-dom/src/test-utils/ReactTestUtils.js
+++ b/packages/react-dom/src/test-utils/ReactTestUtils.js
@@ -42,8 +42,10 @@ const [
   restoreStateIfNeeded,
   dispatchEvent,
   runEventsInBatch,
-  // eslint-disable-next-line no-unused-vars
+  /* eslint-disable no-unused-vars */
   flushPassiveEffects,
+  ReactActingRendererSigil,
+  /* eslint-enable no-unused-vars */
 ] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
 
 function Event(suffix) {}
diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
index f1fe7646626a7..e6dc6e38807f0 100644
--- a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
+++ b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js
@@ -33,11 +33,12 @@ const [
   runEventsInBatch,
   /* eslint-enable no-unused-vars */
   flushPassiveEffects,
+  ReactActingRendererSigil,
 ] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
 
 const batchedUpdates = ReactDOM.unstable_batchedUpdates;
 
-const {ReactActingRendererSigil} = ReactSharedInternals;
+const {ReactCurrentActingRendererSigil} = ReactSharedInternals;
 
 // this implementation should be exactly the same in
 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
@@ -87,17 +88,15 @@ function act(callback: () => Thenable) {
   let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
   let previousActingUpdatesSigil;
   actingUpdatesScopeDepth++;
-  // we use the function flushPassiveEffects directly as the sigil,
-  // since it's unique to a renderer
   if (__DEV__) {
-    previousActingUpdatesSigil = ReactActingRendererSigil.current;
-    ReactActingRendererSigil.current = flushPassiveEffects;
+    previousActingUpdatesSigil = ReactCurrentActingRendererSigil.current;
+    ReactCurrentActingRendererSigil.current = ReactActingRendererSigil;
   }
 
   function onDone() {
     actingUpdatesScopeDepth--;
     if (__DEV__) {
-      ReactActingRendererSigil.current = previousActingUpdatesSigil;
+      ReactCurrentActingRendererSigil.current = previousActingUpdatesSigil;
       if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
         // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
         warningWithoutStack(
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index 3ca5aaa545765..89276a0b494fb 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -81,7 +81,7 @@ type TextInstance = {|
 |};
 type HostContext = Object;
 
-const {ReactActingRendererSigil} = ReactSharedInternals;
+const {ReactCurrentActingRendererSigil} = ReactSharedInternals;
 
 const NO_CONTEXT = {};
 const UPPERCASE_CONTEXT = {};
@@ -650,7 +650,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
   const roots = new Map();
   const DEFAULT_ROOT_ID = '<default>';
 
-  const {flushPassiveEffects, batchedUpdates} = NoopRenderer;
+  const {
+    flushPassiveEffects,
+    batchedUpdates,
+    ReactActingRendererSigil,
+  } = NoopRenderer;
 
   // this act() implementation should be exactly the same in
   // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
@@ -700,17 +704,15 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
     let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
     let previousActingUpdatesSigil;
     actingUpdatesScopeDepth++;
-    // we use the function flushPassiveEffects directly as the sigil,
-    // since it's unique to a renderer
     if (__DEV__) {
-      previousActingUpdatesSigil = ReactActingRendererSigil.current;
-      ReactActingRendererSigil.current = flushPassiveEffects;
+      previousActingUpdatesSigil = ReactCurrentActingRendererSigil.current;
+      ReactCurrentActingRendererSigil.current = ReactActingRendererSigil;
     }
 
     function onDone() {
       actingUpdatesScopeDepth--;
       if (__DEV__) {
-        ReactActingRendererSigil.current = previousActingUpdatesSigil;
+        ReactCurrentActingRendererSigil.current = previousActingUpdatesSigil;
         if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
           // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
           warningWithoutStack(
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index fd313a8e48632..de84dd7b67244 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -57,6 +57,7 @@ import {
   flushDiscreteUpdates,
   flushPassiveEffects,
   warnIfNotScopedWithMatchingAct,
+  ReactActingRendererSigil,
 } from './ReactFiberWorkLoop';
 import {createUpdate, enqueueUpdate} from './ReactUpdateQueue';
 import ReactFiberInstrumentation from './ReactFiberInstrumentation';
@@ -339,6 +340,7 @@ export {
   flushControlled,
   flushSync,
   flushPassiveEffects,
+  ReactActingRendererSigil,
 };
 
 export function getPublicRootInstance(
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index a0edd5a1b8a4e..a9eea7c154b5d 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -173,7 +173,7 @@ const ceil = Math.ceil;
 const {
   ReactCurrentDispatcher,
   ReactCurrentOwner,
-  ReactActingRendererSigil,
+  ReactCurrentActingRendererSigil,
 } = ReactSharedInternals;
 
 type WorkPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6;
@@ -2276,13 +2276,19 @@ function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) {
   }
 }
 
+// We export a simple object here to be used by a renderer/test-utils
+// as the value of ReactCurrentActingRendererSigil.current
+// This identity lets us identify (ha!) when the wrong renderer's act()
+// wraps anothers' updates/effects
+export const ReactActingRendererSigil = {};
+
 export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
   if (__DEV__) {
     if (
-      ReactActingRendererSigil.current !== null &&
+      ReactCurrentActingRendererSigil.current !== null &&
       // use the function flushPassiveEffects directly as the sigil
       // so this comparison is expected here
-      ReactActingRendererSigil.current !== flushPassiveEffects
+      ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
     ) {
       // it looks like we're using the wrong matching act(), so log a warning
       warningWithoutStack(
@@ -2309,7 +2315,7 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
   if (__DEV__) {
     if (
       workPhase === NotWorking &&
-      ReactActingRendererSigil.current !== flushPassiveEffects
+      ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
     ) {
       warningWithoutStack(
         false,
diff --git a/packages/react-test-renderer/src/ReactTestRendererAct.js b/packages/react-test-renderer/src/ReactTestRendererAct.js
index 39087f111028e..ffd55af9eb579 100644
--- a/packages/react-test-renderer/src/ReactTestRendererAct.js
+++ b/packages/react-test-renderer/src/ReactTestRendererAct.js
@@ -11,6 +11,7 @@ import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop';
 import {
   batchedUpdates,
   flushPassiveEffects,
+  ReactActingRendererSigil,
 } from 'react-reconciler/inline.test';
 import ReactSharedInternals from 'shared/ReactSharedInternals';
 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -18,7 +19,7 @@ import {warnAboutMissingMockScheduler} from 'shared/ReactFeatureFlags';
 import enqueueTask from 'shared/enqueueTask';
 import * as Scheduler from 'scheduler';
 
-const {ReactActingRendererSigil} = ReactSharedInternals;
+const {ReactCurrentActingRendererSigil} = ReactSharedInternals;
 
 // this implementation should be exactly the same in
 // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
@@ -68,17 +69,15 @@ function act(callback: () => Thenable) {
   let previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
   let previousActingUpdatesSigil;
   actingUpdatesScopeDepth++;
-  // we use the function flushPassiveEffects directly as the sigil,
-  // since it's unique to a renderer
   if (__DEV__) {
-    previousActingUpdatesSigil = ReactActingRendererSigil.current;
-    ReactActingRendererSigil.current = flushPassiveEffects;
+    previousActingUpdatesSigil = ReactCurrentActingRendererSigil.current;
+    ReactCurrentActingRendererSigil.current = ReactActingRendererSigil;
   }
 
   function onDone() {
     actingUpdatesScopeDepth--;
     if (__DEV__) {
-      ReactActingRendererSigil.current = previousActingUpdatesSigil;
+      ReactCurrentActingRendererSigil.current = previousActingUpdatesSigil;
       if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
         // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
         warningWithoutStack(
diff --git a/packages/react/src/ReactActingRendererSigil.js b/packages/react/src/ReactCurrentActingRendererSigil.js
similarity index 82%
rename from packages/react/src/ReactActingRendererSigil.js
rename to packages/react/src/ReactCurrentActingRendererSigil.js
index a40ecca4c6604..09e05c8362d7b 100644
--- a/packages/react/src/ReactActingRendererSigil.js
+++ b/packages/react/src/ReactCurrentActingRendererSigil.js
@@ -13,7 +13,7 @@
  * so we can track identity of the renderer.
  */
 
-const ReactActingRendererSigil = {
+const ReactCurrentActingRendererSigil = {
   current: (null: null | (() => boolean)),
 };
-export default ReactActingRendererSigil;
+export default ReactCurrentActingRendererSigil;
diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternals.js
index de4a14adb3dbf..1d993139c4194 100644
--- a/packages/react/src/ReactSharedInternals.js
+++ b/packages/react/src/ReactSharedInternals.js
@@ -10,13 +10,13 @@ import ReactCurrentDispatcher from './ReactCurrentDispatcher';
 import ReactCurrentBatchConfig from './ReactCurrentBatchConfig';
 import ReactCurrentOwner from './ReactCurrentOwner';
 import ReactDebugCurrentFrame from './ReactDebugCurrentFrame';
-import ReactActingRendererSigil from './ReactActingRendererSigil';
+import ReactCurrentActingRendererSigil from './ReactCurrentActingRendererSigil';
 
 const ReactSharedInternals = {
   ReactCurrentDispatcher,
   ReactCurrentBatchConfig,
   ReactCurrentOwner,
-  ReactActingRendererSigil,
+  ReactCurrentActingRendererSigil,
   // Used by renderers to avoid bundling object-assign twice in UMD bundles:
   assign,
 };