diff --git a/fixtures/dom/src/__tests__/nested-act-test.js b/fixtures/dom/src/__tests__/nested-act-test.js index 4a39a0ea98f7f..3219706e3fff6 100644 --- a/fixtures/dom/src/__tests__/nested-act-test.js +++ b/fixtures/dom/src/__tests__/nested-act-test.js @@ -48,7 +48,7 @@ describe('unmocked scheduler', () => { TestAct(() => { TestRenderer.create(); }); - expect(log).toEqual(['called']); + expect(log).toEqual([]); }); expect(log).toEqual(['called']); }); diff --git a/fixtures/dom/src/__tests__/wrong-act-test.js b/fixtures/dom/src/__tests__/wrong-act-test.js deleted file mode 100644 index 05f1853f8a2c7..0000000000000 --- a/fixtures/dom/src/__tests__/wrong-act-test.js +++ /dev/null @@ -1,207 +0,0 @@ -/** - * 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 - */ - -let React; -let ReactDOM; -let ReactART; -let TestUtils; -let ARTSVGMode; -let ARTCurrentMode; -let TestRenderer; -let ARTTest; - -global.__DEV__ = process.env.NODE_ENV !== 'production'; -global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental'; - -expect.extend(require('../toWarnDev')); - -function App(props) { - return 'hello world'; -} - -beforeEach(() => { - jest.resetModules(); - React = require('react'); - ReactDOM = require('react-dom'); - TestUtils = require('react-dom/test-utils'); - ReactART = require('react-art'); - ARTSVGMode = require('art/modes/svg'); - ARTCurrentMode = require('art/modes/current'); - TestRenderer = require('react-test-renderer'); - - ARTCurrentMode.setCurrent(ARTSVGMode); - - ARTTest = function ARTTestComponent(props) { - return ( - - - - - M64.564,38.583H54l0.008-5.834c0-3.035,0.293-4.666,4.657-4.666 - h5.833V16.429h-9.33c-11.213,0-15.159,5.654-15.159,15.16v6.994 - h-6.99v11.652h6.99v33.815H54V50.235h9.331L64.564,38.583z - - - - ); - }; -}); - -it("doesn't warn when you use the right act + renderer: dom", () => { - TestUtils.act(() => { - ReactDOM.render(, document.createElement('div')); - }); -}); - -it("doesn't warn when you use the right act + renderer: test", () => { - TestRenderer.act(() => { - TestRenderer.create(); - }); -}); - -it('resets correctly across renderers', async () => { - function Effecty() { - React.useEffect(() => {}, []); - return null; - } - await TestUtils.act(async () => { - TestRenderer.act(() => {}); - expect(() => { - TestRenderer.create(); - }).toWarnDev(["It looks like you're using the wrong act()"], { - withoutStack: true, - }); - }); -}); - -it('warns when using the wrong act version - test + dom: render', () => { - expect(() => { - TestRenderer.act(() => { - ReactDOM.render(, document.createElement('div')); - }); - }).toWarnDev( - [ - 'ReactDOM.render is no longer supported in React 18.', - "It looks like you're using the wrong act()", - ], - { - withoutStack: true, - } - ); -}); - -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; - } - ReactDOM.render(, document.createElement('div')); - expect(() => { - TestRenderer.act(() => { - setCtr(1); - }); - }).toWarnDev(["It looks like you're using the wrong act()"], { - withoutStack: true, - }); -}); - -it('warns when using the wrong act version - dom + test: .create()', () => { - expect(() => { - TestUtils.act(() => { - TestRenderer.create(); - }); - }).toWarnDev(["It looks like you're using the wrong act()"], { - withoutStack: true, - }); -}); - -it('warns when using the wrong act version - dom + test: .update()', () => { - const root = TestRenderer.create(); - expect(() => { - TestUtils.act(() => { - root.update(); - }); - }).toWarnDev(["It looks like you're using the wrong act()"], { - withoutStack: true, - }); -}); - -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; - } - TestRenderer.create(); - expect(() => { - TestUtils.act(() => { - setCtr(1); - }); - }).toWarnDev(["It looks like you're using the wrong act()"], { - withoutStack: true, - }); -}); - -it('does not warn when nesting react-act inside react-dom', () => { - TestUtils.act(() => { - ReactDOM.render(, document.createElement('div')); - }); -}); - -it('does not warn when nesting react-act inside react-test-renderer', () => { - TestRenderer.act(() => { - TestRenderer.create(); - }); -}); - -it("doesn't warn if you use nested acts from different renderers", () => { - TestRenderer.act(() => { - TestUtils.act(() => { - TestRenderer.create(); - }); - }); -}); - -if (__EXPERIMENTAL__) { - it('warns when using createRoot() + .render', () => { - const root = ReactDOM.createRoot(document.createElement('div')); - expect(() => { - TestRenderer.act(() => { - root.render(); - }); - }).toWarnDev( - [ - 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked', - "It looks like you're using the wrong act()", - ], - { - withoutStack: true, - } - ); - }); -} diff --git a/packages/react-dom/src/__tests__/ReactDOMTestSelectors-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMTestSelectors-test.internal.js index 24c3978566a01..e9b0cc6ea0e5b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMTestSelectors-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMTestSelectors-test.internal.js @@ -33,7 +33,7 @@ describe('ReactDOMTestSelectors', () => { React = require('react'); const ReactDOM = require('react-dom/testing'); - act = ReactDOM.act; + act = React.unstable_act; createComponentSelector = ReactDOM.createComponentSelector; createHasPseudoClassSelector = ReactDOM.createHasPseudoClassSelector; createRoleSelector = ReactDOM.createRoleSelector; diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js index 2dabcfc5281b7..0e10207b0bb04 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js @@ -354,32 +354,6 @@ function runActTests(label, render, unmount, rerender) { expect(container.innerHTML).toBe('2'); }); }); - - // @gate __DEV__ - it('warns if you return a value inside act', () => { - expect(() => act(() => null)).toErrorDev( - [ - 'The callback passed to act(...) function must return undefined, or a Promise.', - ], - {withoutStack: true}, - ); - expect(() => act(() => 123)).toErrorDev( - [ - 'The callback passed to act(...) function must return undefined, or a Promise.', - ], - {withoutStack: true}, - ); - }); - - // @gate __DEV__ - it('warns if you try to await a sync .act call', () => { - expect(() => act(() => {}).then(() => {})).toErrorDev( - [ - 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', - ], - {withoutStack: true}, - ); - }); }); describe('asynchronous tests', () => { @@ -401,15 +375,17 @@ function runActTests(label, render, unmount, rerender) { await act(async () => { render(, container); - // flush a little to start the timer - expect(Scheduler).toFlushAndYield([]); + }); + expect(container.innerHTML).toBe('0'); + // Flush the pending timers + await act(async () => { await sleep(100); }); expect(container.innerHTML).toBe('1'); }); // @gate __DEV__ - it('flushes microtasks before exiting', async () => { + it('flushes microtasks before exiting (async function)', async () => { function App() { const [ctr, setCtr] = React.useState(0); async function someAsyncFunction() { @@ -431,6 +407,31 @@ function runActTests(label, render, unmount, rerender) { expect(container.innerHTML).toEqual('1'); }); + // @gate __DEV__ + it('flushes microtasks before exiting (sync function)', async () => { + // Same as previous test, but the callback passed to `act` is not itself + // an async function. + function App() { + const [ctr, setCtr] = React.useState(0); + async function someAsyncFunction() { + // queue a bunch of promises to be sure they all flush + await null; + await null; + await null; + setCtr(1); + } + React.useEffect(() => { + someAsyncFunction(); + }, []); + return ctr; + } + + await act(() => { + render(, container); + }); + expect(container.innerHTML).toEqual('1'); + }); + // @gate __DEV__ it('warns if you do not await an act call', async () => { spyOnDevAndProd(console, 'error'); @@ -461,7 +462,13 @@ function runActTests(label, render, unmount, rerender) { await sleep(150); if (__DEV__) { - expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledTimes(2); + expect(console.error.calls.argsFor(0)[0]).toMatch( + 'You seem to have overlapping act() calls', + ); + expect(console.error.calls.argsFor(1)[0]).toMatch( + 'You seem to have overlapping act() calls', + ); } }); diff --git a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js deleted file mode 100644 index 501b01336b1b3..0000000000000 --- a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.internal.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * 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 - */ - -let React; -let ReactDOM; -let ReactFeatureFlags; - -function App() { - return null; -} - -beforeEach(() => { - jest.resetModules(); - jest.unmock('scheduler'); - React = require('react'); - ReactDOM = require('react-dom'); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.warnAboutUnmockedScheduler = true; -}); - -afterEach(() => { - ReactFeatureFlags.warnAboutUnmockedScheduler = false; -}); - -it('should warn in legacy mode', () => { - expect(() => { - ReactDOM.render(, document.createElement('div')); - }).toErrorDev( - ['Starting from React v18, the "scheduler" module will need to be mocked'], - {withoutStack: true}, - ); - // does not warn twice - expect(() => { - ReactDOM.render(, document.createElement('div')); - }).toErrorDev([]); -}); - -it('does not warn if Scheduler is mocked', () => { - jest.resetModules(); - jest.mock('scheduler', () => require('scheduler/unstable_mock')); - React = require('react'); - ReactDOM = require('react-dom'); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.warnAboutUnmockedScheduler = true; - - // This should not warn - expect(() => { - ReactDOM.render(, document.createElement('div')); - }).toErrorDev([]); -}); diff --git a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js deleted file mode 100644 index 960154dbea253..0000000000000 --- a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * 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 - */ - -let React; -let ReactDOM; - -function App() { - return null; -} - -beforeEach(() => { - jest.resetModules(); - jest.unmock('scheduler'); - React = require('react'); - ReactDOM = require('react-dom'); -}); - -it('does not warn when rendering in legacy mode', () => { - expect(() => { - ReactDOM.render(, document.createElement('div')); - }).toErrorDev([]); -}); - -it('should warn when rendering in concurrent mode', () => { - expect(() => { - ReactDOM.createRoot(document.createElement('div')).render(); - }).toErrorDev( - 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + - 'to guarantee consistent behaviour across tests and browsers.', - {withoutStack: true}, - ); - // does not warn twice - expect(() => { - ReactDOM.createRoot(document.createElement('div')).render(); - }).toErrorDev([]); -}); diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index eade4ea474c77..e603121d82c08 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -29,7 +29,6 @@ import { flushControlled, injectIntoDevTools, IsThisRendererActing, - act, attemptSynchronousHydration, attemptDiscreteHydration, attemptContinuousHydration, @@ -163,8 +162,8 @@ const Internals = { getFiberCurrentPropsFromNode, enqueueStateRestore, restoreStateIfNeeded, + batchedUpdates, ], - act, // TODO: Temporary. Only used by our internal version of `act. Will remove. IsThisRendererActing, }; diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index 69c1a6afcff3a..e721dd30e7a34 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -34,8 +34,14 @@ const getNodeFromInstance = EventInternals[1]; const getFiberCurrentPropsFromNode = EventInternals[2]; const enqueueStateRestore = EventInternals[3]; const restoreStateIfNeeded = EventInternals[4]; +const batchedUpdates = EventInternals[5]; -const act = SecretInternals.act; +const act_notBatchedInLegacyMode = React.unstable_act; +function act(callback) { + return act_notBatchedInLegacyMode(() => { + return batchedUpdates(callback); + }); +} function Event(suffix) {} diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsInternalAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsInternalAct.js index 3adc786eed5fa..6afe4af8aa49e 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtilsInternalAct.js +++ b/packages/react-dom/src/test-utils/ReactTestUtilsInternalAct.js @@ -20,7 +20,7 @@ const IsThisRendererActing = SecretInternals.IsThisRendererActing; const batchedUpdates = ReactDOM.unstable_batchedUpdates; -const {IsSomeRendererActing} = ReactSharedInternals; +const {IsSomeRendererActing, ReactCurrentActQueue} = ReactSharedInternals; // This version of `act` is only used by our tests. Unlike the public version // of `act`, it's designed to work identically in both production and @@ -28,6 +28,8 @@ const {IsSomeRendererActing} = ReactSharedInternals; // version, too, since our constraints in our test suite are not the same as // those of developers using React — we're testing React itself, as opposed to // building an app with React. +// TODO: Replace the internal "concurrent" implementations of `act` with a +// single shared module. let actingUpdatesScopeDepth = 0; @@ -50,8 +52,14 @@ export function unstable_concurrentAct(scope: () => Thenable | void) { IsSomeRendererActing.current = true; IsThisRendererActing.current = true; actingUpdatesScopeDepth++; + if (__DEV__ && actingUpdatesScopeDepth === 1) { + ReactCurrentActQueue.disableActWarning = true; + } const unwind = () => { + if (__DEV__ && actingUpdatesScopeDepth === 1) { + ReactCurrentActQueue.disableActWarning = false; + } actingUpdatesScopeDepth--; IsSomeRendererActing.current = previousIsSomeRendererActing; IsThisRendererActing.current = previousIsThisRendererActing; diff --git a/packages/react-dom/testing.classic.fb.js b/packages/react-dom/testing.classic.fb.js index 923ba78f09d1a..a127b63f8c8f6 100644 --- a/packages/react-dom/testing.classic.fb.js +++ b/packages/react-dom/testing.classic.fb.js @@ -9,7 +9,6 @@ export * from './index.classic.fb.js'; export { - act, createComponentSelector, createHasPseudoClassSelector, createRoleSelector, diff --git a/packages/react-dom/testing.experimental.js b/packages/react-dom/testing.experimental.js index 4d3f2d2a7f4f2..f994d83fa7d22 100644 --- a/packages/react-dom/testing.experimental.js +++ b/packages/react-dom/testing.experimental.js @@ -8,4 +8,3 @@ */ export * from './index.experimental.js'; -export {act} from 'react-reconciler/src/ReactFiberReconciler'; diff --git a/packages/react-dom/testing.js b/packages/react-dom/testing.js index d66124f67d83e..ef4ac5899dfd8 100644 --- a/packages/react-dom/testing.js +++ b/packages/react-dom/testing.js @@ -9,7 +9,6 @@ export * from './index.js'; export { - act, createComponentSelector, createHasPseudoClassSelector, createRoleSelector, diff --git a/packages/react-dom/testing.modern.fb.js b/packages/react-dom/testing.modern.fb.js index 7f22bb849e7c6..738ae3a3710f8 100644 --- a/packages/react-dom/testing.modern.fb.js +++ b/packages/react-dom/testing.modern.fb.js @@ -9,7 +9,6 @@ export * from './index.modern.fb.js'; export { - act, createComponentSelector, createHasPseudoClassSelector, createRoleSelector, diff --git a/packages/react-dom/testing.stable.js b/packages/react-dom/testing.stable.js index 47161fadd7f88..044b15bde9c4d 100644 --- a/packages/react-dom/testing.stable.js +++ b/packages/react-dom/testing.stable.js @@ -8,4 +8,3 @@ */ export * from './index.stable.js'; -export {act} from 'react-reconciler/src/ReactFiberReconciler'; diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 9d2c87621bcef..59cea7cd5d971 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -31,7 +31,7 @@ import { import ReactSharedInternals from 'shared/ReactSharedInternals'; import enqueueTask from 'shared/enqueueTask'; -const {IsSomeRendererActing} = ReactSharedInternals; +const {IsSomeRendererActing, ReactCurrentActQueue} = ReactSharedInternals; type Container = { rootID: string, @@ -1048,6 +1048,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { // version, too, since our constraints in our test suite are not the same as // those of developers using React — we're testing React itself, as opposed to // building an app with React. + // TODO: Replace the internal "concurrent" implementations of `act` with a + // single shared module. const {batchedUpdates, IsThisRendererActing} = NoopRenderer; let actingUpdatesScopeDepth = 0; @@ -1071,8 +1073,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { IsSomeRendererActing.current = true; IsThisRendererActing.current = true; actingUpdatesScopeDepth++; + if (__DEV__ && actingUpdatesScopeDepth === 1) { + ReactCurrentActQueue.disableActWarning = true; + } const unwind = () => { + if (__DEV__ && actingUpdatesScopeDepth === 1) { + ReactCurrentActQueue.disableActWarning = false; + } actingUpdatesScopeDepth--; IsSomeRendererActing.current = previousIsSomeRendererActing; IsThisRendererActing.current = previousIsThisRendererActing; diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 394aa983174c4..b195a0cb57488 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -79,7 +79,6 @@ import { requestEventTime, warnIfNotCurrentlyActingEffectsInDEV, warnIfNotCurrentlyActingUpdatesInDev, - warnIfNotScopedWithMatchingAct, markSkippedUpdateLanes, isInterleavedUpdate, } from './ReactFiberWorkLoop.new'; @@ -2011,7 +2010,6 @@ function dispatchAction( if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests if ('undefined' !== typeof jest) { - warnIfNotScopedWithMatchingAct(fiber); warnIfNotCurrentlyActingUpdatesInDev(fiber); } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index a5eac6836e283..7c51c43ad2e7e 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -79,7 +79,6 @@ import { requestEventTime, warnIfNotCurrentlyActingEffectsInDEV, warnIfNotCurrentlyActingUpdatesInDev, - warnIfNotScopedWithMatchingAct, markSkippedUpdateLanes, isInterleavedUpdate, } from './ReactFiberWorkLoop.old'; @@ -2011,7 +2010,6 @@ function dispatchAction( if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests 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 ff68770f09b06..6675ed78d5ffb 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -38,7 +38,6 @@ import { shouldError as shouldError_old, shouldSuspend as shouldSuspend_old, injectIntoDevTools as injectIntoDevTools_old, - act as act_old, createPortal as createPortal_old, createComponentSelector as createComponentSelector_old, createHasPseudoClassSelector as createHasPseudoClassSelector_old, @@ -79,7 +78,6 @@ import { shouldError as shouldError_new, shouldSuspend as shouldSuspend_new, injectIntoDevTools as injectIntoDevTools_new, - act as act_new, createPortal as createPortal_new, createComponentSelector as createComponentSelector_new, createHasPseudoClassSelector as createHasPseudoClassSelector_new, @@ -166,7 +164,6 @@ export const shouldSuspend = enableNewReconciler export const injectIntoDevTools = enableNewReconciler ? injectIntoDevTools_new : injectIntoDevTools_old; -export const act = enableNewReconciler ? act_new : act_old; export const createPortal = enableNewReconciler ? createPortal_new : createPortal_old; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index c53fabbb8e4ae..9e831157bf5fc 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -60,10 +60,7 @@ import { discreteUpdates, flushDiscreteUpdates, flushPassiveEffects, - warnIfNotScopedWithMatchingAct, - warnIfUnmockedScheduler, IsThisRendererActing, - act, } from './ReactFiberWorkLoop.new'; import { createUpdate, @@ -272,13 +269,6 @@ export function updateContainer( } const current = container.current; const eventTime = requestEventTime(); - if (__DEV__) { - // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests - if ('undefined' !== typeof jest) { - warnIfUnmockedScheduler(current); - warnIfNotScopedWithMatchingAct(current); - } - } const lane = requestUpdateLane(current); if (enableSchedulingProfiler) { @@ -348,7 +338,6 @@ export { flushSync, flushPassiveEffects, IsThisRendererActing, - act, }; export function getPublicRootInstance( diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 73d412bfcd1ae..8accca3deb359 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -60,10 +60,7 @@ import { discreteUpdates, flushDiscreteUpdates, flushPassiveEffects, - warnIfNotScopedWithMatchingAct, - warnIfUnmockedScheduler, IsThisRendererActing, - act, } from './ReactFiberWorkLoop.old'; import { createUpdate, @@ -272,13 +269,6 @@ export function updateContainer( } const current = container.current; const eventTime = requestEventTime(); - if (__DEV__) { - // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests - if ('undefined' !== typeof jest) { - warnIfUnmockedScheduler(current); - warnIfNotScopedWithMatchingAct(current); - } - } const lane = requestUpdateLane(current); if (enableSchedulingProfiler) { @@ -348,7 +338,6 @@ export { flushSync, flushPassiveEffects, IsThisRendererActing, - act, }; export function getPublicRootInstance( diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index eb26348ca16e6..b4f889c569afe 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -7,7 +7,7 @@ * @flow */ -import type {Thenable, Wakeable} from 'shared/ReactTypes'; +import type {Wakeable} from 'shared/ReactTypes'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.new'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; @@ -24,7 +24,6 @@ import { enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, enableProfilerNestedUpdateScheduledHook, - warnAboutUnmockedScheduler, deferRenderPhaseUpdateToNextBatch, enableDebugTracing, enableSchedulingProfiler, @@ -37,8 +36,9 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; import { - scheduleCallback, - cancelCallback, + // Aliased because `act` will override and push to an internal queue + scheduleCallback as Scheduler_scheduleCallback, + cancelCallback as Scheduler_cancelCallback, shouldYield, requestPaint, now, @@ -79,9 +79,6 @@ import { markRenderStopped, } from './SchedulingProfiler'; -// The scheduler is imported here *only* to detect whether it's been mocked -import * as Scheduler from 'scheduler'; - import { resetAfterCommit, scheduleTimeout, @@ -238,16 +235,13 @@ import { } from './ReactFiberDevToolsHook.new'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; -// Used by `act` -import enqueueTask from 'shared/enqueueTask'; - const ceil = Math.ceil; const { ReactCurrentDispatcher, ReactCurrentOwner, ReactCurrentBatchConfig, - IsSomeRendererActing, + ReactCurrentActQueue, } = ReactSharedInternals; type ExecutionContext = number; @@ -653,7 +647,17 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { // Check if there's an existing task. We may be able to reuse it. const existingCallbackPriority = root.callbackPriority; - if (existingCallbackPriority === newCallbackPriority) { + if ( + existingCallbackPriority === newCallbackPriority && + // Special case related to `act`. If the currently scheduled task is a + // Scheduler task, rather than an `act` task, cancel it and re-scheduled + // on the `act` queue. + !( + __DEV__ && + ReactCurrentActQueue.current !== null && + existingCallbackNode !== fakeActCallbackNode + ) + ) { if (__DEV__) { // If we're going to re-use an existing task, it needs to exist. // Assume that discrete update microtasks are non-cancellable and null. @@ -2781,42 +2785,35 @@ export function restorePendingUpdaters(root: FiberRoot, lanes: Lanes): void { } } -export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void { +const fakeActCallbackNode = {}; +function scheduleCallback(priorityLevel, callback) { if (__DEV__) { - if ( - warnsIfNotActing === true && - IsSomeRendererActing.current === true && - IsThisRendererActing.current !== true - ) { - const previousFiber = ReactCurrentFiberCurrent; - try { - setCurrentDebugFiberInDEV(fiber); - console.error( - "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' + - // Break up imports to avoid accidentally parsing them as dependencies. - 'import {act} fr' + - "om 'react-dom/test-utils';\n" + - '// ...\n' + - 'act(() => ...);\n\n' + - '// for react-test-renderer:\n' + - // Break up imports to avoid accidentally parsing them as dependencies. - 'import TestRenderer fr' + - "om 'react-test-renderer';\n" + - 'const {act} = TestRenderer;\n' + - '// ...\n' + - 'act(() => ...);', - ); - } finally { - if (previousFiber) { - setCurrentDebugFiberInDEV(fiber); - } else { - resetCurrentDebugFiberInDEV(); - } - } + // If we're currently inside an `act` scope, bypass Scheduler and push to + // the `act` queue instead. + const actQueue = ReactCurrentActQueue.current; + if (actQueue !== null) { + actQueue.push(callback); + return fakeActCallbackNode; + } else { + return Scheduler_scheduleCallback(priorityLevel, callback); } + } else { + // In production, always call Scheduler. This function will be stripped out. + return Scheduler_scheduleCallback(priorityLevel, callback); + } +} + +function cancelCallback(callbackNode) { + if (__DEV__ && callbackNode === fakeActCallbackNode) { + return; } + // In production, always call Scheduler. This function will be stripped out. + return Scheduler_cancelCallback(callbackNode); +} + +function shouldForceFlushFallbacksInDEV() { + // Never force flush in production. This function should get stripped out. + return __DEV__ && ReactCurrentActQueue.current !== null; } export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { @@ -2824,8 +2821,13 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { if ( warnsIfNotActing === true && (fiber.mode & StrictLegacyMode) !== NoMode && - IsSomeRendererActing.current === false && - IsThisRendererActing.current === false + ReactCurrentActQueue.current === null && + // Our internal tests use a custom implementation of `act` that works by + // mocking the Scheduler package. Disable the `act` warning. + // TODO: Maybe the warning should be disabled by default, and then turned + // on at the testing frameworks layer? Instead of what we do now, which + // is check if a `jest` global is defined. + ReactCurrentActQueue.disableActWarning === false ) { console.error( 'An update to %s ran an effect, but was not wrapped in act(...).\n\n' + @@ -2849,8 +2851,13 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { if ( warnsIfNotActing === true && executionContext === NoContext && - IsSomeRendererActing.current === false && - IsThisRendererActing.current === false + ReactCurrentActQueue.current === null && + // Our internal tests use a custom implementation of `act` that works by + // mocking the Scheduler package. Disable the `act` warning. + // TODO: Maybe the warning should be disabled by default, and then turned + // on at the testing frameworks layer? Instead of what we do now, which + // is check if a `jest` global is defined. + ReactCurrentActQueue.disableActWarning === false ) { const previousFiber = ReactCurrentFiberCurrent; try { @@ -2880,252 +2887,3 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { } export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV; - -// In tests, we want to enforce a mocked scheduler. -let didWarnAboutUnmockedScheduler = false; -// TODO Before we release concurrent mode, revisit this and decide whether a mocked -// scheduler is the actual recommendation. The alternative could be a testing build, -// a new lib, or whatever; we dunno just yet. This message is for early adopters -// to get their tests right. - -export function warnIfUnmockedScheduler(fiber: Fiber) { - if (__DEV__) { - if ( - didWarnAboutUnmockedScheduler === false && - Scheduler.unstable_flushAllWithoutAsserting === undefined - ) { - if (fiber.mode & ConcurrentMode) { - didWarnAboutUnmockedScheduler = true; - console.error( - 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + - 'to guarantee consistent behaviour across tests and browsers. ' + - 'For example, with jest: \n' + - // Break up requires to avoid accidentally parsing them as dependencies. - "jest.mock('scheduler', () => require" + - "('scheduler/unstable_mock'));\n\n" + - 'For more info, visit https://reactjs.org/link/mock-scheduler', - ); - } else if (warnAboutUnmockedScheduler === true) { - didWarnAboutUnmockedScheduler = true; - console.error( - 'Starting from React v18, the "scheduler" module will need to be mocked ' + - 'to guarantee consistent behaviour across tests and browsers. ' + - 'For example, with jest: \n' + - // Break up requires to avoid accidentally parsing them as dependencies. - "jest.mock('scheduler', () => require" + - "('scheduler/unstable_mock'));\n\n" + - 'For more info, visit https://reactjs.org/link/mock-scheduler', - ); - } - } - } -} - -// `act` testing API -// -// TODO: This is mostly a copy-paste from the legacy `act`, which does not have -// access to the same internals that we do here. Some trade offs in the -// implementation no longer make sense. - -let isFlushingAct = false; -let isInsideThisAct = false; - -function shouldForceFlushFallbacksInDEV() { - // Never force flush in production. This function should get stripped out. - return __DEV__ && actingUpdatesScopeDepth > 0; -} - -const flushMockScheduler = Scheduler.unstable_flushAllWithoutAsserting; -const isSchedulerMocked = typeof flushMockScheduler === 'function'; - -// Returns whether additional work was scheduled. Caller should keep flushing -// until there's no work left. -function flushActWork(): boolean { - if (flushMockScheduler !== undefined) { - const prevIsFlushing = isFlushingAct; - isFlushingAct = true; - try { - return flushMockScheduler(); - } finally { - isFlushingAct = prevIsFlushing; - } - } else { - // No mock scheduler available. However, the only type of pending work is - // passive effects, which we control. So we can flush that. - const prevIsFlushing = isFlushingAct; - isFlushingAct = true; - try { - let didFlushWork = false; - while (flushPassiveEffects()) { - didFlushWork = true; - } - return didFlushWork; - } finally { - isFlushingAct = prevIsFlushing; - } - } -} - -function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) { - try { - flushActWork(); - enqueueTask(() => { - if (flushActWork()) { - flushWorkAndMicroTasks(onDone); - } else { - onDone(); - } - }); - } catch (err) { - onDone(err); - } -} - -// we track the 'depth' of the act() calls with this counter, -// so we can tell if any async act() calls try to run in parallel. - -let actingUpdatesScopeDepth = 0; - -export function act(callback: () => Thenable): Thenable { - if (!__DEV__) { - invariant( - false, - 'act(...) is not supported in production builds of React.', - ); - } - - const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth; - actingUpdatesScopeDepth++; - - const previousIsSomeRendererActing = IsSomeRendererActing.current; - const previousIsThisRendererActing = IsThisRendererActing.current; - const previousIsInsideThisAct = isInsideThisAct; - IsSomeRendererActing.current = true; - IsThisRendererActing.current = true; - isInsideThisAct = true; - - function onDone() { - actingUpdatesScopeDepth--; - IsSomeRendererActing.current = previousIsSomeRendererActing; - IsThisRendererActing.current = previousIsThisRendererActing; - isInsideThisAct = previousIsInsideThisAct; - if (__DEV__) { - if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) { - // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned - console.error( - 'You seem to have overlapping act() calls, this is not supported. ' + - 'Be sure to await previous act() calls before making a new one. ', - ); - } - } - } - - let result; - try { - result = batchedUpdates(callback); - } catch (error) { - // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth - onDone(); - throw error; - } - - if ( - result !== null && - typeof result === 'object' && - typeof result.then === 'function' - ) { - // setup a boolean that gets set to true only - // once this act() call is await-ed - let called = false; - if (__DEV__) { - if (typeof Promise !== 'undefined') { - //eslint-disable-next-line no-undef - Promise.resolve() - .then(() => {}) - .then(() => { - if (called === false) { - console.error( - 'You called act(async () => ...) without await. ' + - 'This could lead to unexpected testing behaviour, interleaving multiple act ' + - 'calls and mixing their scopes. You should - await act(async () => ...);', - ); - } - }); - } - } - - // in the async case, the returned thenable runs the callback, flushes - // effects and microtasks in a loop until flushPassiveEffects() === false, - // and cleans up - return { - then(resolve, reject) { - called = true; - result.then( - () => { - if ( - actingUpdatesScopeDepth > 1 || - (isSchedulerMocked === true && - previousIsSomeRendererActing === true) - ) { - onDone(); - resolve(); - return; - } - // we're about to exit the act() scope, - // now's the time to flush tasks/effects - flushWorkAndMicroTasks((err: ?Error) => { - onDone(); - if (err) { - reject(err); - } else { - resolve(); - } - }); - }, - err => { - onDone(); - reject(err); - }, - ); - }, - }; - } else { - if (__DEV__) { - if (result !== undefined) { - console.error( - 'The callback passed to act(...) function ' + - 'must return undefined, or a Promise. You returned %s', - result, - ); - } - } - - // flush effects until none remain, and cleanup - try { - if ( - actingUpdatesScopeDepth === 1 && - (isSchedulerMocked === false || previousIsSomeRendererActing === false) - ) { - // we're about to exit the act() scope, - // now's the time to flush effects - flushActWork(); - } - onDone(); - } catch (err) { - onDone(); - throw err; - } - - // in the sync case, the returned thenable only warns *if* await-ed - return { - then(resolve) { - if (__DEV__) { - console.error( - 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', - ); - } - resolve(); - }, - }; - } -} diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 0b34957078535..d2ccf9e6826af 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -7,7 +7,7 @@ * @flow */ -import type {Thenable, Wakeable} from 'shared/ReactTypes'; +import type {Wakeable} from 'shared/ReactTypes'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.old'; import type {SuspenseState} from './ReactFiberSuspenseComponent.old'; @@ -24,7 +24,6 @@ import { enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, enableProfilerNestedUpdateScheduledHook, - warnAboutUnmockedScheduler, deferRenderPhaseUpdateToNextBatch, enableDebugTracing, enableSchedulingProfiler, @@ -37,8 +36,9 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; import { - scheduleCallback, - cancelCallback, + // Aliased because `act` will override and push to an internal queue + scheduleCallback as Scheduler_scheduleCallback, + cancelCallback as Scheduler_cancelCallback, shouldYield, requestPaint, now, @@ -79,9 +79,6 @@ import { markRenderStopped, } from './SchedulingProfiler'; -// The scheduler is imported here *only* to detect whether it's been mocked -import * as Scheduler from 'scheduler'; - import { resetAfterCommit, scheduleTimeout, @@ -238,16 +235,13 @@ import { } from './ReactFiberDevToolsHook.old'; import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; -// Used by `act` -import enqueueTask from 'shared/enqueueTask'; - const ceil = Math.ceil; const { ReactCurrentDispatcher, ReactCurrentOwner, ReactCurrentBatchConfig, - IsSomeRendererActing, + ReactCurrentActQueue, } = ReactSharedInternals; type ExecutionContext = number; @@ -653,7 +647,17 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { // Check if there's an existing task. We may be able to reuse it. const existingCallbackPriority = root.callbackPriority; - if (existingCallbackPriority === newCallbackPriority) { + if ( + existingCallbackPriority === newCallbackPriority && + // Special case related to `act`. If the currently scheduled task is a + // Scheduler task, rather than an `act` task, cancel it and re-scheduled + // on the `act` queue. + !( + __DEV__ && + ReactCurrentActQueue.current !== null && + existingCallbackNode !== fakeActCallbackNode + ) + ) { if (__DEV__) { // If we're going to re-use an existing task, it needs to exist. // Assume that discrete update microtasks are non-cancellable and null. @@ -2781,42 +2785,35 @@ export function restorePendingUpdaters(root: FiberRoot, lanes: Lanes): void { } } -export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void { +const fakeActCallbackNode = {}; +function scheduleCallback(priorityLevel, callback) { if (__DEV__) { - if ( - warnsIfNotActing === true && - IsSomeRendererActing.current === true && - IsThisRendererActing.current !== true - ) { - const previousFiber = ReactCurrentFiberCurrent; - try { - setCurrentDebugFiberInDEV(fiber); - console.error( - "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' + - // Break up imports to avoid accidentally parsing them as dependencies. - 'import {act} fr' + - "om 'react-dom/test-utils';\n" + - '// ...\n' + - 'act(() => ...);\n\n' + - '// for react-test-renderer:\n' + - // Break up imports to avoid accidentally parsing them as dependencies. - 'import TestRenderer fr' + - "om 'react-test-renderer';\n" + - 'const {act} = TestRenderer;\n' + - '// ...\n' + - 'act(() => ...);', - ); - } finally { - if (previousFiber) { - setCurrentDebugFiberInDEV(fiber); - } else { - resetCurrentDebugFiberInDEV(); - } - } + // If we're currently inside an `act` scope, bypass Scheduler and push to + // the `act` queue instead. + const actQueue = ReactCurrentActQueue.current; + if (actQueue !== null) { + actQueue.push(callback); + return fakeActCallbackNode; + } else { + return Scheduler_scheduleCallback(priorityLevel, callback); } + } else { + // In production, always call Scheduler. This function will be stripped out. + return Scheduler_scheduleCallback(priorityLevel, callback); + } +} + +function cancelCallback(callbackNode) { + if (__DEV__ && callbackNode === fakeActCallbackNode) { + return; } + // In production, always call Scheduler. This function will be stripped out. + return Scheduler_cancelCallback(callbackNode); +} + +function shouldForceFlushFallbacksInDEV() { + // Never force flush in production. This function should get stripped out. + return __DEV__ && ReactCurrentActQueue.current !== null; } export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { @@ -2824,8 +2821,13 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { if ( warnsIfNotActing === true && (fiber.mode & StrictLegacyMode) !== NoMode && - IsSomeRendererActing.current === false && - IsThisRendererActing.current === false + ReactCurrentActQueue.current === null && + // Our internal tests use a custom implementation of `act` that works by + // mocking the Scheduler package. Disable the `act` warning. + // TODO: Maybe the warning should be disabled by default, and then turned + // on at the testing frameworks layer? Instead of what we do now, which + // is check if a `jest` global is defined. + ReactCurrentActQueue.disableActWarning === false ) { console.error( 'An update to %s ran an effect, but was not wrapped in act(...).\n\n' + @@ -2849,8 +2851,13 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { if ( warnsIfNotActing === true && executionContext === NoContext && - IsSomeRendererActing.current === false && - IsThisRendererActing.current === false + ReactCurrentActQueue.current === null && + // Our internal tests use a custom implementation of `act` that works by + // mocking the Scheduler package. Disable the `act` warning. + // TODO: Maybe the warning should be disabled by default, and then turned + // on at the testing frameworks layer? Instead of what we do now, which + // is check if a `jest` global is defined. + ReactCurrentActQueue.disableActWarning === false ) { const previousFiber = ReactCurrentFiberCurrent; try { @@ -2880,252 +2887,3 @@ function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void { } export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV; - -// In tests, we want to enforce a mocked scheduler. -let didWarnAboutUnmockedScheduler = false; -// TODO Before we release concurrent mode, revisit this and decide whether a mocked -// scheduler is the actual recommendation. The alternative could be a testing build, -// a new lib, or whatever; we dunno just yet. This message is for early adopters -// to get their tests right. - -export function warnIfUnmockedScheduler(fiber: Fiber) { - if (__DEV__) { - if ( - didWarnAboutUnmockedScheduler === false && - Scheduler.unstable_flushAllWithoutAsserting === undefined - ) { - if (fiber.mode & ConcurrentMode) { - didWarnAboutUnmockedScheduler = true; - console.error( - 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + - 'to guarantee consistent behaviour across tests and browsers. ' + - 'For example, with jest: \n' + - // Break up requires to avoid accidentally parsing them as dependencies. - "jest.mock('scheduler', () => require" + - "('scheduler/unstable_mock'));\n\n" + - 'For more info, visit https://reactjs.org/link/mock-scheduler', - ); - } else if (warnAboutUnmockedScheduler === true) { - didWarnAboutUnmockedScheduler = true; - console.error( - 'Starting from React v18, the "scheduler" module will need to be mocked ' + - 'to guarantee consistent behaviour across tests and browsers. ' + - 'For example, with jest: \n' + - // Break up requires to avoid accidentally parsing them as dependencies. - "jest.mock('scheduler', () => require" + - "('scheduler/unstable_mock'));\n\n" + - 'For more info, visit https://reactjs.org/link/mock-scheduler', - ); - } - } - } -} - -// `act` testing API -// -// TODO: This is mostly a copy-paste from the legacy `act`, which does not have -// access to the same internals that we do here. Some trade offs in the -// implementation no longer make sense. - -let isFlushingAct = false; -let isInsideThisAct = false; - -function shouldForceFlushFallbacksInDEV() { - // Never force flush in production. This function should get stripped out. - return __DEV__ && actingUpdatesScopeDepth > 0; -} - -const flushMockScheduler = Scheduler.unstable_flushAllWithoutAsserting; -const isSchedulerMocked = typeof flushMockScheduler === 'function'; - -// Returns whether additional work was scheduled. Caller should keep flushing -// until there's no work left. -function flushActWork(): boolean { - if (flushMockScheduler !== undefined) { - const prevIsFlushing = isFlushingAct; - isFlushingAct = true; - try { - return flushMockScheduler(); - } finally { - isFlushingAct = prevIsFlushing; - } - } else { - // No mock scheduler available. However, the only type of pending work is - // passive effects, which we control. So we can flush that. - const prevIsFlushing = isFlushingAct; - isFlushingAct = true; - try { - let didFlushWork = false; - while (flushPassiveEffects()) { - didFlushWork = true; - } - return didFlushWork; - } finally { - isFlushingAct = prevIsFlushing; - } - } -} - -function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) { - try { - flushActWork(); - enqueueTask(() => { - if (flushActWork()) { - flushWorkAndMicroTasks(onDone); - } else { - onDone(); - } - }); - } catch (err) { - onDone(err); - } -} - -// we track the 'depth' of the act() calls with this counter, -// so we can tell if any async act() calls try to run in parallel. - -let actingUpdatesScopeDepth = 0; - -export function act(callback: () => Thenable): Thenable { - if (!__DEV__) { - invariant( - false, - 'act(...) is not supported in production builds of React.', - ); - } - - const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth; - actingUpdatesScopeDepth++; - - const previousIsSomeRendererActing = IsSomeRendererActing.current; - const previousIsThisRendererActing = IsThisRendererActing.current; - const previousIsInsideThisAct = isInsideThisAct; - IsSomeRendererActing.current = true; - IsThisRendererActing.current = true; - isInsideThisAct = true; - - function onDone() { - actingUpdatesScopeDepth--; - IsSomeRendererActing.current = previousIsSomeRendererActing; - IsThisRendererActing.current = previousIsThisRendererActing; - isInsideThisAct = previousIsInsideThisAct; - if (__DEV__) { - if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) { - // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned - console.error( - 'You seem to have overlapping act() calls, this is not supported. ' + - 'Be sure to await previous act() calls before making a new one. ', - ); - } - } - } - - let result; - try { - result = batchedUpdates(callback); - } catch (error) { - // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth - onDone(); - throw error; - } - - if ( - result !== null && - typeof result === 'object' && - typeof result.then === 'function' - ) { - // setup a boolean that gets set to true only - // once this act() call is await-ed - let called = false; - if (__DEV__) { - if (typeof Promise !== 'undefined') { - //eslint-disable-next-line no-undef - Promise.resolve() - .then(() => {}) - .then(() => { - if (called === false) { - console.error( - 'You called act(async () => ...) without await. ' + - 'This could lead to unexpected testing behaviour, interleaving multiple act ' + - 'calls and mixing their scopes. You should - await act(async () => ...);', - ); - } - }); - } - } - - // in the async case, the returned thenable runs the callback, flushes - // effects and microtasks in a loop until flushPassiveEffects() === false, - // and cleans up - return { - then(resolve, reject) { - called = true; - result.then( - () => { - if ( - actingUpdatesScopeDepth > 1 || - (isSchedulerMocked === true && - previousIsSomeRendererActing === true) - ) { - onDone(); - resolve(); - return; - } - // we're about to exit the act() scope, - // now's the time to flush tasks/effects - flushWorkAndMicroTasks((err: ?Error) => { - onDone(); - if (err) { - reject(err); - } else { - resolve(); - } - }); - }, - err => { - onDone(); - reject(err); - }, - ); - }, - }; - } else { - if (__DEV__) { - if (result !== undefined) { - console.error( - 'The callback passed to act(...) function ' + - 'must return undefined, or a Promise. You returned %s', - result, - ); - } - } - - // flush effects until none remain, and cleanup - try { - if ( - actingUpdatesScopeDepth === 1 && - (isSchedulerMocked === false || previousIsSomeRendererActing === false) - ) { - // we're about to exit the act() scope, - // now's the time to flush effects - flushActWork(); - } - onDone(); - } catch (err) { - onDone(); - throw err; - } - - // in the sync case, the returned thenable only warns *if* await-ed - return { - then(resolve) { - if (__DEV__) { - console.error( - 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', - ); - } - resolve(); - }, - }; - } -} diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index 8348c708f3bc5..35c22e672ed3e 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js @@ -11,6 +11,7 @@ 'use strict'; let React; +let act; let ReactFiberReconciler; let ConcurrentRoot; let DefaultEventPriority; @@ -19,6 +20,7 @@ describe('ReactFiberHostContext', () => { beforeEach(() => { jest.resetModules(); React = require('react'); + act = React.unstable_act; ReactFiberReconciler = require('react-reconciler'); ConcurrentRoot = require('react-reconciler/src/ReactRootTags') .ConcurrentRoot; @@ -71,7 +73,7 @@ describe('ReactFiberHostContext', () => { false, null, ); - Renderer.act(() => { + act(() => { Renderer.updateContainer( @@ -132,7 +134,7 @@ describe('ReactFiberHostContext', () => { false, null, ); - Renderer.act(() => { + act(() => { Renderer.updateContainer( diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 246790ead3a46..469807090ec62 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -12,6 +12,7 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type {Instance, TextInstance} from './ReactTestHostConfig'; +import * as React from 'react'; import * as Scheduler from 'scheduler/unstable_mock'; import { getPublicRootInstance, @@ -20,7 +21,6 @@ import { flushSync, injectIntoDevTools, batchedUpdates, - act, IsThisRendererActing, } from 'react-reconciler/src/ReactFiberReconciler'; import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection'; @@ -53,7 +53,14 @@ import {getPublicInstance} from './ReactTestHostConfig'; import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags'; import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags'; -const {IsSomeRendererActing} = ReactSharedInternals; +const {IsSomeRendererActing, ReactCurrentActQueue} = ReactSharedInternals; + +const act_notBatchedInLegacyMode = React.unstable_act; +function act(callback: () => Thenable): Thenable { + return act_notBatchedInLegacyMode(() => { + return batchedUpdates(callback); + }); +} type TestRendererOptions = { createNodeMock: (element: React$Element) => any, @@ -604,6 +611,8 @@ let actingUpdatesScopeDepth = 0; // building an app with React. // TODO: Migrate our tests to use ReactNoop. Although we would need to figure // out a solution for Relay, which has some Concurrent Mode tests. +// TODO: Replace the internal "concurrent" implementations of `act` with a +// single shared module. function unstable_concurrentAct(scope: () => Thenable | void) { if (Scheduler.unstable_flushAllWithoutAsserting === undefined) { throw Error( @@ -623,8 +632,14 @@ function unstable_concurrentAct(scope: () => Thenable | void) { IsSomeRendererActing.current = true; IsThisRendererActing.current = true; actingUpdatesScopeDepth++; + if (__DEV__ && actingUpdatesScopeDepth === 1) { + ReactCurrentActQueue.disableActWarning = true; + } const unwind = () => { + if (__DEV__ && actingUpdatesScopeDepth === 1) { + ReactCurrentActQueue.disableActWarning = false; + } actingUpdatesScopeDepth--; IsSomeRendererActing.current = previousIsSomeRendererActing; IsThisRendererActing.current = previousIsThisRendererActing; diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js index 28cc0062e8a7b..4fe1afea6d62f 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js @@ -40,23 +40,6 @@ describe('ReactTestRenderer.act()', () => { expect(root.toJSON()).toEqual('1'); }); - it("warns if you don't use .act", () => { - let setCtr; - function App(props) { - const [ctr, _setCtr] = React.useState(0); - setCtr = _setCtr; - return ctr; - } - - ReactTestRenderer.create(); - - expect(() => { - setCtr(1); - }).toErrorDev([ - 'An update to App inside a test was not wrapped in act(...)', - ]); - }); - describe('async', () => { // @gate __DEV__ it('should work with async/await', async () => { diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 15f3447a111f8..653013c7b0797 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -9,6 +9,7 @@ export { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + act as unstable_act, Children, Component, Fragment, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index 5c7197b282cdc..ab90ea66bc112 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -9,6 +9,7 @@ export { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + act as unstable_act, Children, Component, Fragment, diff --git a/packages/react/index.js b/packages/react/index.js index 59e76eff92fa3..247d17ec01b8d 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -33,6 +33,7 @@ export type ChildrenArray<+T> = $ReadOnlyArray> | T; // We can't use export * from in Flow for some reason. export { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + act as unstable_act, Children, Component, Fragment, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 47b629832b332..4f316eacad8b7 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -9,6 +9,7 @@ export { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + act as unstable_act, Children, Component, Fragment, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 33e1b37693bd1..008875e4577e5 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -9,6 +9,7 @@ export { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + act as unstable_act, Children, Component, Fragment, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index a99d2a331b20a..2b87d18b6c81d 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -60,6 +60,7 @@ import { import {createMutableSource} from './ReactMutableSource'; import ReactSharedInternals from './ReactSharedInternals'; import {startTransition} from './ReactStartTransition'; +import {act} from './ReactAct'; // TODO: Move this branching into the other module instead and just re-export. const createElement = __DEV__ ? createElementWithValidation : createElementProd; @@ -120,4 +121,5 @@ export { // enableScopeAPI REACT_SCOPE_TYPE as unstable_Scope, useOpaqueIdentifier as unstable_useOpaqueIdentifier, + act, }; diff --git a/packages/react/src/ReactAct.js b/packages/react/src/ReactAct.js new file mode 100644 index 0000000000000..885e803b027c5 --- /dev/null +++ b/packages/react/src/ReactAct.js @@ -0,0 +1,194 @@ +/** + * 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 + */ + +import type {Thenable} from 'shared/ReactTypes'; +import ReactCurrentActQueue from './ReactCurrentActQueue'; +import invariant from 'shared/invariant'; +import enqueueTask from 'shared/enqueueTask'; + +let actScopeDepth = 0; +let didWarnNoAwaitAct = false; + +export function act(callback: () => Thenable): Thenable { + if (__DEV__) { + // `act` calls can be nested, so we track the depth. This represents the + // number of `act` scopes on the stack. + const prevActScopeDepth = actScopeDepth; + actScopeDepth++; + + if (ReactCurrentActQueue.current === null) { + // This is the outermost `act` scope. Initialize the queue. The reconciler + // will detect the queue and use it instead of Scheduler. + ReactCurrentActQueue.current = []; + } + + let result; + try { + result = callback(); + } catch (error) { + popActScope(prevActScopeDepth); + throw error; + } + + if ( + result !== null && + typeof result === 'object' && + typeof result.then === 'function' + ) { + // The callback is an async function (i.e. returned a promise). Wait + // for it to resolve before exiting the current scope. + let wasAwaited = false; + const thenable = { + then(resolve, reject) { + wasAwaited = true; + result.then( + () => { + popActScope(prevActScopeDepth); + if (actScopeDepth === 0) { + // We've exited the outermost act scope. Recursively flush the + // queue until there's no remaining work. + recursivelyFlushAsyncActWork(resolve, reject); + } else { + resolve(); + } + }, + error => { + // The callback threw an error. + popActScope(prevActScopeDepth); + reject(error); + }, + ); + }, + }; + + if (__DEV__) { + if (!didWarnNoAwaitAct && typeof Promise !== 'undefined') { + // eslint-disable-next-line no-undef + Promise.resolve() + .then(() => {}) + .then(() => { + if (!wasAwaited) { + didWarnNoAwaitAct = true; + console.error( + 'You called act(async () => ...) without await. ' + + 'This could lead to unexpected testing behaviour, ' + + 'interleaving multiple act calls and mixing their ' + + 'scopes. ' + + 'You should - await act(async () => ...);', + ); + } + }); + } + } + return thenable; + } else { + // The callback is not an async function. Exit the current scope + // immediately, without awaiting. + popActScope(prevActScopeDepth); + if (actScopeDepth === 0) { + // Exiting the outermost act scope. Flush the queue. + const queue = ReactCurrentActQueue.current; + if (queue !== null) { + flushActQueue(queue); + ReactCurrentActQueue.current = null; + } + // Return a thenable. If the user awaits it, we'll flush again in + // case additional work was scheduled by a microtask. + return { + then(resolve, reject) { + // Confirm we haven't re-entered another `act` scope, in case + // the user does something weird like await the thenable + // multiple times. + if (ReactCurrentActQueue.current === null) { + // Recursively flush the queue until there's no remaining work. + ReactCurrentActQueue.current = []; + recursivelyFlushAsyncActWork(resolve, reject); + } + }, + }; + } else { + // Since we're inside a nested `act` scope, the returned thenable + // immediately resolves. The outer scope will flush the queue. + return { + then(resolve, reject) { + resolve(); + }, + }; + } + } + } else { + invariant( + false, + 'act(...) is not supported in production builds of React.', + ); + } +} + +function popActScope(prevActScopeDepth) { + if (__DEV__) { + if (prevActScopeDepth !== actScopeDepth - 1) { + console.error( + 'You seem to have overlapping act() calls, this is not supported. ' + + 'Be sure to await previous act() calls before making a new one. ', + ); + } + actScopeDepth = prevActScopeDepth; + } +} + +function recursivelyFlushAsyncActWork(resolve, reject) { + if (__DEV__) { + const queue = ReactCurrentActQueue.current; + if (queue !== null) { + try { + flushActQueue(queue); + enqueueTask(() => { + if (queue.length === 0) { + // No additional work was scheduled. Finish. + ReactCurrentActQueue.current = null; + resolve(); + } else { + // Keep flushing work until there's none left. + recursivelyFlushAsyncActWork(resolve, reject); + } + }); + } catch (error) { + reject(error); + } + } else { + resolve(); + } + } +} + +let isFlushing = false; +function flushActQueue(queue) { + if (__DEV__) { + if (!isFlushing) { + // Prevent re-entrancy. + isFlushing = true; + let i = 0; + try { + for (; i < queue.length; i++) { + let callback = queue[i]; + do { + callback = callback(true); + } while (callback !== null); + } + queue.length = 0; + } catch (error) { + // If something throws, leave the remaining callbacks on the queue. + queue = queue.slice(i + 1); + throw error; + } finally { + isFlushing = false; + } + } + } +} diff --git a/packages/react/src/ReactCurrentActQueue.js b/packages/react/src/ReactCurrentActQueue.js new file mode 100644 index 0000000000000..1b7966337cad5 --- /dev/null +++ b/packages/react/src/ReactCurrentActQueue.js @@ -0,0 +1,22 @@ +/** + * 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 + */ + +type RendererTask = boolean => RendererTask | null; + +const ReactCurrentActQueue = { + current: (null: null | Array), + // Our internal tests use a custom implementation of `act` that works by + // mocking the Scheduler package. Use this field to disable the `act` warning. + // TODO: Maybe the warning should be disabled by default, and then turned + // on at the testing frameworks layer? Instead of what we do now, which + // is check if a `jest` global is defined. + disableActWarning: (false: boolean), +}; + +export default ReactCurrentActQueue; diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternals.js index 791d41adf8044..e95ea15848751 100644 --- a/packages/react/src/ReactSharedInternals.js +++ b/packages/react/src/ReactSharedInternals.js @@ -8,6 +8,7 @@ import assign from 'object-assign'; import ReactCurrentDispatcher from './ReactCurrentDispatcher'; import ReactCurrentBatchConfig from './ReactCurrentBatchConfig'; +import ReactCurrentActQueue from './ReactCurrentActQueue'; import ReactCurrentOwner from './ReactCurrentOwner'; import ReactDebugCurrentFrame from './ReactDebugCurrentFrame'; import IsSomeRendererActing from './IsSomeRendererActing'; @@ -23,6 +24,7 @@ const ReactSharedInternals = { if (__DEV__) { ReactSharedInternals.ReactDebugCurrentFrame = ReactDebugCurrentFrame; + ReactSharedInternals.ReactCurrentActQueue = ReactCurrentActQueue; } export default ReactSharedInternals; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 60efb97e42017..d966c9ec54c9d 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -78,7 +78,6 @@ export const enableCreateEventHandleAPI = false; // We will enforce mocking scheduler with scheduler/unstable_mock at some point. (v18?) // Till then, we warn about the missing mock, but still fallback to a legacy mode compatible version -export const warnAboutUnmockedScheduler = false; // Add a callback property to suspense to notify which promises are currently // in the update queue. This allows reporting and tracing of what is causing diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index a58e9e543895f..76cfa7d18f3ea 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -30,7 +30,6 @@ export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; export const warnAboutDeprecatedLifecycles = true; export const enableScopeAPI = false; export const enableCreateEventHandleAPI = false; -export const warnAboutUnmockedScheduler = true; export const enableSuspenseCallback = false; export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 1fc0969a3fa90..f8595c01b9274 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -29,7 +29,6 @@ export const disableInputAttributeSyncing = false; export const enableSchedulerDebugging = false; export const enableScopeAPI = false; export const enableCreateEventHandleAPI = false; -export const warnAboutUnmockedScheduler = false; export const enableSuspenseCallback = false; export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index fe3abe658bdd4..528f42ddc3cc0 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -29,7 +29,6 @@ export const disableInputAttributeSyncing = false; export const enableSchedulerDebugging = false; export const enableScopeAPI = false; export const enableCreateEventHandleAPI = false; -export const warnAboutUnmockedScheduler = false; export const enableSuspenseCallback = false; export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index bdf3607a1ee82..6a70e11a4c557 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -29,7 +29,6 @@ export const disableInputAttributeSyncing = false; export const enableSchedulerDebugging = false; export const enableScopeAPI = false; export const enableCreateEventHandleAPI = false; -export const warnAboutUnmockedScheduler = false; export const enableSuspenseCallback = false; export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index a4f209a6b9f30..ef095eef7e7bd 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -29,7 +29,6 @@ export const disableJavaScriptURLs = false; export const disableInputAttributeSyncing = false; export const enableScopeAPI = true; export const enableCreateEventHandleAPI = false; -export const warnAboutUnmockedScheduler = true; export const enableSuspenseCallback = true; export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 5c8baa8a12049..58c6400b0aeef 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -29,7 +29,6 @@ export const disableInputAttributeSyncing = false; export const enableSchedulerDebugging = false; export const enableScopeAPI = false; export const enableCreateEventHandleAPI = false; -export const warnAboutUnmockedScheduler = false; export const enableSuspenseCallback = false; export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 30cf572ddce21..0c3d15890b277 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -29,7 +29,6 @@ export const disableInputAttributeSyncing = false; export const enableSchedulerDebugging = false; export const enableScopeAPI = true; export const enableCreateEventHandleAPI = true; -export const warnAboutUnmockedScheduler = true; export const enableSuspenseCallback = true; export const warnAboutDefaultPropsOnFunctionComponents = false; export const warnAboutStringRefs = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 04abc20de6464..e028c9ddb8ef9 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -75,8 +75,6 @@ export const enableCreateEventHandleAPI = true; export const enableScopeAPI = true; -export const warnAboutUnmockedScheduler = true; - export const enableSuspenseCallback = true; export const enableComponentStackLocations = true; diff --git a/scripts/jest/setupTests.www.js b/scripts/jest/setupTests.www.js index 3812d8f1354b5..e303d9fee22a5 100644 --- a/scripts/jest/setupTests.www.js +++ b/scripts/jest/setupTests.www.js @@ -14,7 +14,6 @@ jest.mock('shared/ReactFeatureFlags', () => { // www configuration. Update those tests so that they work against the www // configuration, too. Then remove these overrides. wwwFlags.disableLegacyContext = defaultFlags.disableLegacyContext; - wwwFlags.warnAboutUnmockedScheduler = defaultFlags.warnAboutUnmockedScheduler; wwwFlags.disableJavaScriptURLs = defaultFlags.disableJavaScriptURLs; return wwwFlags;