mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
This took a while, but I'm happy I went through it. Some key moments - recursively flushing effects, flushing microtasks on each async turn, and my team's uncompromising philosophy on code reuse. Really happy with this. I still want to expand test coverage, and I have some more small related todos, but this is good to land. On to the next one. Soundtrack to landing this - https://open.spotify.com/track/0MF8I8OUo8kytiOo8aSHYq?si=gSWqUheKQbiQDXzptCXHTg * hacked up act(async () => {...}) * move stuff around * merge changes * abstract .act warnings and stuff. all renderers. pass all tests. * move testutils.act back into testutils * move into scheduler, rename some bits * smaller bundle * a comment for why we don't do typeof === 'function' * fix test * pass tests - fire, prod * lose actContainerElement * tighter * write a test for TestRenderer it's an odd one, because not only does sync act not flush effects correctly, but the async one does (wut). verified it's fine with the dom version. * lint * rewrote to move flushing logic closer to the renderer the scheduler's `flushPassiveEffects` didn't work as expected for the test renderer, so I decided to go back to the hack (rendering a dumb container) This also makes reactdom not as heavy (by a few bytes, but still). * move it around so the delta isn't too bad * cleanups fix promise chaining propagate errors correctly test for thenable the 'right' way more tests! tidier! ponies! * Stray comment * recursively flush effects * fixed tests * lint, move noop.act into react-reconciler * microtasks when checking if called, s/called/calledLog, cleanup * pass fb lint we could have globally changed our eslint config to assume Promise is available, but that means we expect a promise polyfill on the page, and we don't yet. this code is triggered only in jest anyway, and we're fairly certain Promise will be available there. hence, the once-off disable for the check * shorter timers, fix a test, test for Promise * use global.Promise for existence check * flush microtasks * a version that works in browsers (that support postMessage) I also added a sanity fixture inside fixtures/dom/ mostly for me. * hoist flushEffectsAndMicroTasks * pull out tick logic from ReactFiberScheduler * fix await act (...sync) hanging - fix a hang when awaiting sync logic - a better async/await test for test renderer * feedback changes - use node's setImmediate if available - a warning if MessageChannel isn't available - rename some functions * pass lint/flow checks (without requiring a Promise polyfill/exclusion) * prettier the prettiest, even. * use globalPromise for the missed await warning * __DEV__ check for didWarnAboutMessageChannel * thenables and callbacks instead of promises, pass flow/lint * tinier. better. - pulled most bits out of FiberScheduler - actedUpdates uses callbacks now * pass build validation * augh prettier * golfing 7 more chars * Test that effects are not flushed without also flushing microtasks * export doesHavePendingPassiveEffects, nits * createAct() * dead code * missed in merge? * lose the preflushing bits * ugh prettier * removed `actedUpdates()`, created shared/actingUpdatesScopeDepth * rearrange imports so builds work, remove the hack versions of flushPassiveEffects * represent actingUpdatesScopeDepth as a tuple [number] * use a shared flag on React.__SECRET... * remove createAct, setup act for all relevant renderers * review feedback shared/enqueueTask import ReactSharedInternals from 'shared/ReactSharedInternals'; simpler act() internals ReactSharedInternals.ReactShouldWarnActingUpdates * move act() implementation into createReactNoop * warnIfNotCurrentlyActingUpdatesInDev condition check order
154 lines
4.2 KiB
JavaScript
154 lines
4.2 KiB
JavaScript
/**
|
|
* 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 'react-reconciler/src/ReactFiberScheduler';
|
|
|
|
import {
|
|
batchedUpdates,
|
|
flushPassiveEffects,
|
|
} from 'react-reconciler/inline.test';
|
|
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
|
import warningWithoutStack from 'shared/warningWithoutStack';
|
|
import enqueueTask from 'shared/enqueueTask';
|
|
|
|
const {ReactShouldWarnActingUpdates} = ReactSharedInternals;
|
|
|
|
// this implementation should be exactly the same in
|
|
// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js
|
|
|
|
// 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;
|
|
|
|
function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) {
|
|
try {
|
|
flushPassiveEffects();
|
|
enqueueTask(() => {
|
|
if (flushPassiveEffects()) {
|
|
flushEffectsAndMicroTasks(onDone);
|
|
} else {
|
|
onDone();
|
|
}
|
|
});
|
|
} catch (err) {
|
|
onDone(err);
|
|
}
|
|
}
|
|
|
|
function act(callback: () => Thenable) {
|
|
let previousActingUpdatesScopeDepth;
|
|
if (__DEV__) {
|
|
previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
|
|
actingUpdatesScopeDepth++;
|
|
ReactShouldWarnActingUpdates.current = true;
|
|
}
|
|
|
|
function onDone() {
|
|
if (__DEV__) {
|
|
actingUpdatesScopeDepth--;
|
|
if (actingUpdatesScopeDepth === 0) {
|
|
ReactShouldWarnActingUpdates.current = false;
|
|
}
|
|
if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
|
|
// if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
|
|
warningWithoutStack(
|
|
null,
|
|
'You seem to have overlapping act() calls, this is not supported. ' +
|
|
'Be sure to await previous act() calls before making a new one. ',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = batchedUpdates(callback);
|
|
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) {
|
|
warningWithoutStack(
|
|
null,
|
|
'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: () => void, reject: (?Error) => void) {
|
|
called = true;
|
|
result.then(
|
|
() => {
|
|
flushEffectsAndMicroTasks((err: ?Error) => {
|
|
onDone();
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
},
|
|
err => {
|
|
onDone();
|
|
reject(err);
|
|
},
|
|
);
|
|
},
|
|
};
|
|
} else {
|
|
if (__DEV__) {
|
|
warningWithoutStack(
|
|
result === undefined,
|
|
'The callback passed to act(...) function ' +
|
|
'must return undefined, or a Promise. You returned %s',
|
|
result,
|
|
);
|
|
}
|
|
|
|
// flush effects until none remain, and cleanup
|
|
try {
|
|
while (flushPassiveEffects()) {}
|
|
onDone();
|
|
} catch (err) {
|
|
onDone();
|
|
throw err;
|
|
}
|
|
|
|
// in the sync case, the returned thenable only warns *if* await-ed
|
|
return {
|
|
then(resolve: () => void) {
|
|
if (__DEV__) {
|
|
warningWithoutStack(
|
|
false,
|
|
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
|
|
);
|
|
}
|
|
resolve();
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
export default act;
|