`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:
parent
422e0bb360
commit
06f7b4f43a
|
@ -48,7 +48,7 @@ describe('unmocked scheduler', () => {
|
|||
TestAct(() => {
|
||||
TestRenderer.create(<Effecty />);
|
||||
});
|
||||
expect(log).toEqual(['called']);
|
||||
expect(log).toEqual([]);
|
||||
});
|
||||
expect(log).toEqual(['called']);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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([]);
|
||||
});
|
|
@ -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([]);
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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) {}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
export * from './index.classic.fb.js';
|
||||
export {
|
||||
act,
|
||||
createComponentSelector,
|
||||
createHasPseudoClassSelector,
|
||||
createRoleSelector,
|
||||
|
|
|
@ -8,4 +8,3 @@
|
|||
*/
|
||||
|
||||
export * from './index.experimental.js';
|
||||
export {act} from 'react-reconciler/src/ReactFiberReconciler';
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
export * from './index.js';
|
||||
export {
|
||||
act,
|
||||
createComponentSelector,
|
||||
createHasPseudoClassSelector,
|
||||
createRoleSelector,
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
export * from './index.modern.fb.js';
|
||||
export {
|
||||
act,
|
||||
createComponentSelector,
|
||||
createHasPseudoClassSelector,
|
||||
createRoleSelector,
|
||||
|
|
|
@ -8,4 +8,3 @@
|
|||
*/
|
||||
|
||||
export * from './index.stable.js';
|
||||
export {act} from 'react-reconciler/src/ReactFiberReconciler';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
export {
|
||||
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
||||
act as unstable_act,
|
||||
Children,
|
||||
Component,
|
||||
Fragment,
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
export {
|
||||
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
||||
act as unstable_act,
|
||||
Children,
|
||||
Component,
|
||||
Fragment,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
export {
|
||||
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
||||
act as unstable_act,
|
||||
Children,
|
||||
Component,
|
||||
Fragment,
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
export {
|
||||
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
||||
act as unstable_act,
|
||||
Children,
|
||||
Component,
|
||||
Fragment,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -75,8 +75,6 @@ export const enableCreateEventHandleAPI = true;
|
|||
|
||||
export const enableScopeAPI = true;
|
||||
|
||||
export const warnAboutUnmockedScheduler = true;
|
||||
|
||||
export const enableSuspenseCallback = true;
|
||||
|
||||
export const enableComponentStackLocations = true;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue