`act` should work without mock Scheduler (#21714)

Currently, in a React 18 root, `act` only works if you mock the
Scheduler package. This was because we didn't want to add additional
checks at runtime.

But now that the `act` testing API is dev-only, we can simplify its
implementation.

Now when an update is wrapped with `act`, React will bypass Scheduler
entirely and push its tasks onto a special internal queue. Then, when
the outermost `act` scope exists, we'll flush that queue.

I also removed the "wrong act" warning, because the plan is to move
`act` to an isomorphic entry point, simlar to `startTransition`. That's
not directly related to this PR, but I didn't want to bother
re-implementing that warning only to immediately remove it.

I'll add the isomorphic API in a follow up.

Note that the internal version of `act` that we use in our own tests
still depends on mocking the Scheduler package, because it needs to work
in production. I'm planning to move that implementation to a shared
(internal) module, too.
This commit is contained in:
Andrew Clark 2021-06-22 17:25:07 -04:00 committed by GitHub
parent 422e0bb360
commit 06f7b4f43a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 423 additions and 1004 deletions

View File

@ -48,7 +48,7 @@ describe('unmocked scheduler', () => {
TestAct(() => {
TestRenderer.create(<Effecty />);
});
expect(log).toEqual(['called']);
expect(log).toEqual([]);
});
expect(log).toEqual(['called']);
});

View File

@ -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 (
<ReactART.Surface width={150} height={200}>
<ReactART.Group>
<ReactART.Shape
d="M0,0l50,0l0,50l-50,0z"
fill={new ReactART.LinearGradient(['black', 'white'])}
key="a"
width={50}
height={50}
x={50}
y={50}
opacity={0.1}
/>
<ReactART.Shape
fill="#3C5A99"
key="b"
scale={0.5}
x={50}
y={50}
title="This is an F"
cursor="pointer">
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
</ReactART.Shape>
</ReactART.Group>
</ReactART.Surface>
);
};
});
it("doesn't warn when you use the right act + renderer: dom", () => {
TestUtils.act(() => {
ReactDOM.render(<App />, document.createElement('div'));
});
});
it("doesn't warn when you use the right act + renderer: test", () => {
TestRenderer.act(() => {
TestRenderer.create(<App />);
});
});
it('resets correctly across renderers', async () => {
function Effecty() {
React.useEffect(() => {}, []);
return null;
}
await TestUtils.act(async () => {
TestRenderer.act(() => {});
expect(() => {
TestRenderer.create(<Effecty />);
}).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(<App />, 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(<Counter />, 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(<App />);
});
}).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(<App key="one" />);
expect(() => {
TestUtils.act(() => {
root.update(<App key="two" />);
});
}).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(<Counter />);
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(<ARTTest />, document.createElement('div'));
});
});
it('does not warn when nesting react-act inside react-test-renderer', () => {
TestRenderer.act(() => {
TestRenderer.create(<ARTTest />);
});
});
it("doesn't warn if you use nested acts from different renderers", () => {
TestRenderer.act(() => {
TestUtils.act(() => {
TestRenderer.create(<App />);
});
});
});
if (__EXPERIMENTAL__) {
it('warns when using createRoot() + .render', () => {
const root = ReactDOM.createRoot(document.createElement('div'));
expect(() => {
TestRenderer.act(() => {
root.render(<App />);
});
}).toWarnDev(
[
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked',
"It looks like you're using the wrong act()",
],
{
withoutStack: true,
}
);
});
}

View File

@ -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;

View File

@ -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(<App />, 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(<App />, 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',
);
}
});

View File

@ -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(<App />, 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(<App />, 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(<App />, document.createElement('div'));
}).toErrorDev([]);
});

View File

@ -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(<App />, document.createElement('div'));
}).toErrorDev([]);
});
it('should warn when rendering in concurrent mode', () => {
expect(() => {
ReactDOM.createRoot(document.createElement('div')).render(<App />);
}).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(<App />);
}).toErrorDev([]);
});

View File

@ -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,
};

View File

@ -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) {}

View File

@ -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<mixed> | 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;

View File

@ -9,7 +9,6 @@
export * from './index.classic.fb.js';
export {
act,
createComponentSelector,
createHasPseudoClassSelector,
createRoleSelector,

View File

@ -8,4 +8,3 @@
*/
export * from './index.experimental.js';
export {act} from 'react-reconciler/src/ReactFiberReconciler';

View File

@ -9,7 +9,6 @@
export * from './index.js';
export {
act,
createComponentSelector,
createHasPseudoClassSelector,
createRoleSelector,

View File

@ -9,7 +9,6 @@
export * from './index.modern.fb.js';
export {
act,
createComponentSelector,
createHasPseudoClassSelector,
createRoleSelector,

View File

@ -8,4 +8,3 @@
*/
export * from './index.stable.js';
export {act} from 'react-reconciler/src/ReactFiberReconciler';

View File

@ -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;

View File

@ -79,7 +79,6 @@ import {
requestEventTime,
warnIfNotCurrentlyActingEffectsInDEV,
warnIfNotCurrentlyActingUpdatesInDev,
warnIfNotScopedWithMatchingAct,
markSkippedUpdateLanes,
isInterleavedUpdate,
} from './ReactFiberWorkLoop.new';
@ -2011,7 +2010,6 @@ function dispatchAction<S, A>(
if (__DEV__) {
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotScopedWithMatchingAct(fiber);
warnIfNotCurrentlyActingUpdatesInDev(fiber);
}
}

View File

@ -79,7 +79,6 @@ import {
requestEventTime,
warnIfNotCurrentlyActingEffectsInDEV,
warnIfNotCurrentlyActingUpdatesInDev,
warnIfNotScopedWithMatchingAct,
markSkippedUpdateLanes,
isInterleavedUpdate,
} from './ReactFiberWorkLoop.old';
@ -2011,7 +2010,6 @@ function dispatchAction<S, A>(
if (__DEV__) {
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotScopedWithMatchingAct(fiber);
warnIfNotCurrentlyActingUpdatesInDev(fiber);
}
}

View File

@ -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;

View File

@ -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(

View File

@ -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(

View File

@ -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,51 +2785,49 @@ 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 {
if (__DEV__) {
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<mixed>): Thenable<void> {
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();
},
};
}
}

View File

@ -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,51 +2785,49 @@ 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 {
if (__DEV__) {
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<mixed>): Thenable<void> {
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();
},
};
}
}

View File

@ -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(
<a>
<b />
@ -132,7 +134,7 @@ describe('ReactFiberHostContext', () => {
false,
null,
);
Renderer.act(() => {
act(() => {
Renderer.updateContainer(
<a>
<b />

View File

@ -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<mixed>): Thenable<void> {
return act_notBatchedInLegacyMode(() => {
return batchedUpdates(callback);
});
}
type TestRendererOptions = {
createNodeMock: (element: React$Element<any>) => 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<mixed> | void) {
if (Scheduler.unstable_flushAllWithoutAsserting === undefined) {
throw Error(
@ -623,8 +632,14 @@ function unstable_concurrentAct(scope: () => Thenable<mixed> | 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;

View File

@ -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(<App />);
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 () => {

View File

@ -9,6 +9,7 @@
export {
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
act as unstable_act,
Children,
Component,
Fragment,

View File

@ -9,6 +9,7 @@
export {
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
act as unstable_act,
Children,
Component,
Fragment,

View File

@ -33,6 +33,7 @@ export type ChildrenArray<+T> = $ReadOnlyArray<ChildrenArray<T>> | 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,

View File

@ -9,6 +9,7 @@
export {
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
act as unstable_act,
Children,
Component,
Fragment,

View File

@ -9,6 +9,7 @@
export {
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
act as unstable_act,
Children,
Component,
Fragment,

View File

@ -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,
};

View File

@ -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<mixed>): Thenable<void> {
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;
}
}
}
}

View File

@ -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<RendererTask>),
// 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;

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -75,8 +75,6 @@ export const enableCreateEventHandleAPI = true;
export const enableScopeAPI = true;
export const warnAboutUnmockedScheduler = true;
export const enableSuspenseCallback = true;
export const enableComponentStackLocations = true;

View File

@ -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;