7
* @flow
8
*/
9
10
-import type {Thenable, Wakeable} from 'shared/ReactTypes';
10
+import type {Wakeable} from 'shared/ReactTypes';
11
import type {Fiber, FiberRoot} from './ReactInternalTypes';
12
import type {Lanes, Lane} from './ReactFiberLane.new';
13
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
24
enableProfilerCommitHooks,
25
enableProfilerNestedUpdatePhase,
26
enableProfilerNestedUpdateScheduledHook,
27
- warnAboutUnmockedScheduler,
27
deferRenderPhaseUpdateToNextBatch,
28
enableDebugTracing,
29
enableSchedulingProfiler,
36
import invariant from 'shared/invariant';
37
38
import {
40
- scheduleCallback,
41
- cancelCallback,
39
+ // Aliased because `act` will override and push to an internal queue
40
+ scheduleCallback as Scheduler_scheduleCallback,
41
+ cancelCallback as Scheduler_cancelCallback,
42
shouldYield,
43
requestPaint,
44
now,
79
markRenderStopped,
80
} from './SchedulingProfiler';
81
82
-// The scheduler is imported here *only* to detect whether it's been mocked
83
-import * as Scheduler from 'scheduler';
84
-
82
import {
83
resetAfterCommit,
84
scheduleTimeout,
235
} from './ReactFiberDevToolsHook.new';
236
import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
237
241
-// Used by `act`
242
-import enqueueTask from 'shared/enqueueTask';
243
-
238
const ceil = Math.ceil;
239
240
const {
241
ReactCurrentDispatcher,
242
ReactCurrentOwner,
243
ReactCurrentBatchConfig,
250
- IsSomeRendererActing,
244
+ ReactCurrentActQueue,
245
} = ReactSharedInternals;
246
247
type ExecutionContext = number;
647
648
// Check if there's an existing task. We may be able to reuse it.
649
const existingCallbackPriority = root.callbackPriority;
656
- if (existingCallbackPriority === newCallbackPriority) {
650
+ if (
651
+ existingCallbackPriority === newCallbackPriority &&
652
+ // Special case related to `act`. If the currently scheduled task is a
653
+ // Scheduler task, rather than an `act` task, cancel it and re-scheduled
654
+ // on the `act` queue.
655
+ !(
656
+ __DEV__ &&
657
+ ReactCurrentActQueue.current !== null &&
658
+ existingCallbackNode !== fakeActCallbackNode
659
+ )
660
+ ) {
661
if (__DEV__) {
662
// If we're going to re-use an existing task, it needs to exist.
663
// Assume that discrete update microtasks are non-cancellable and null.
2785
}
2786
}
2787
2784
-export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
2788
+const fakeActCallbackNode = {};
2789
+function scheduleCallback(priorityLevel, callback) {
2790
if (__DEV__) {
2786
- if (
2787
- warnsIfNotActing === true &&
2788
- IsSomeRendererActing.current === true &&
2789
- IsThisRendererActing.current !== true
2790
- ) {
2791
- const previousFiber = ReactCurrentFiberCurrent;
2792
- try {
2793
- setCurrentDebugFiberInDEV(fiber);
2794
- console.error(
2795
- "It looks like you're using the wrong act() around your test interactions.\n" +
2796
- 'Be sure to use the matching version of act() corresponding to your renderer:\n\n' +
2797
- '// for react-dom:\n' +
2798
- // Break up imports to avoid accidentally parsing them as dependencies.
2799
- 'import {act} fr' +
2800
- "om 'react-dom/test-utils';\n" +
2801
- '// ...\n' +
2802
- 'act(() => ...);\n\n' +
2803
- '// for react-test-renderer:\n' +
2804
- // Break up imports to avoid accidentally parsing them as dependencies.
2805
- 'import TestRenderer fr' +
2806
- "om 'react-test-renderer';\n" +
2807
- 'const {act} = TestRenderer;\n' +
2808
- '// ...\n' +
2809
- 'act(() => ...);',
2810
- );
2811
- } finally {
2812
- if (previousFiber) {
2813
- setCurrentDebugFiberInDEV(fiber);
2814
- } else {
2815
- resetCurrentDebugFiberInDEV();
2816
- }
2817
- }
2791
+ // If we're currently inside an `act` scope, bypass Scheduler and push to
2792
+ // the `act` queue instead.
2793
+ const actQueue = ReactCurrentActQueue.current;
2794
+ if (actQueue !== null) {
2795
+ actQueue.push(callback);
2796
+ return fakeActCallbackNode;
2797
+ } else {
2798
+ return Scheduler_scheduleCallback(priorityLevel, callback);
2799
}
2800
+ } else {
2801
+ // In production, always call Scheduler. This function will be stripped out.
2802
+ return Scheduler_scheduleCallback(priorityLevel, callback);
2803
+ }
2804
+}
2805
+
2806
+function cancelCallback(callbackNode) {
2807
+ if (__DEV__ && callbackNode === fakeActCallbackNode) {
2808
+ return;
2809
}
2810
+ // In production, always call Scheduler. This function will be stripped out.
2811
+ return Scheduler_cancelCallback(callbackNode);
2812
+}
2813
+
2814
+function shouldForceFlushFallbacksInDEV() {
2815
+ // Never force flush in production. This function should get stripped out.
2816
+ return __DEV__ && ReactCurrentActQueue.current !== null;
2817
}
2818
2819
export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
2821
if (
2822
warnsIfNotActing === true &&
2823
(fiber.mode & StrictLegacyMode) !== NoMode &&
2827
- IsSomeRendererActing.current === false &&
2828
- IsThisRendererActing.current === false
2824
+ ReactCurrentActQueue.current === null &&
2825
+ // Our internal tests use a custom implementation of `act` that works by
2826
+ // mocking the Scheduler package. Disable the `act` warning.
2827
+ // TODO: Maybe the warning should be disabled by default, and then turned
2828
+ // on at the testing frameworks layer? Instead of what we do now, which
2829
+ // is check if a `jest` global is defined.
2830
+ ReactCurrentActQueue.disableActWarning === false
2831
) {
2832
console.error(
2833
'An update to %s ran an effect, but was not wrapped in act(...).\n\n' +
2851
if (
2852
warnsIfNotActing === true &&
2853
executionContext === NoContext &&
2852
- IsSomeRendererActing.current === false &&
2853
- IsThisRendererActing.current === false
2854
+ ReactCurrentActQueue.current === null &&
2855
+ // Our internal tests use a custom implementation of `act` that works by
2856
+ // mocking the Scheduler package. Disable the `act` warning.
2857
+ // TODO: Maybe the warning should be disabled by default, and then turned
2858
+ // on at the testing frameworks layer? Instead of what we do now, which
2859
+ // is check if a `jest` global is defined.
2860
+ ReactCurrentActQueue.disableActWarning === false
2861
) {
2862
const previousFiber = ReactCurrentFiberCurrent;
2863
try {
2887
}
2888
2889
export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV;
2883
-
2884
-// In tests, we want to enforce a mocked scheduler.
2885
-let didWarnAboutUnmockedScheduler = false;
2886
-// TODO Before we release concurrent mode, revisit this and decide whether a mocked
2887
-// scheduler is the actual recommendation. The alternative could be a testing build,
2888
-// a new lib, or whatever; we dunno just yet. This message is for early adopters
2889
-// to get their tests right.
2890
-
2891
-export function warnIfUnmockedScheduler(fiber: Fiber) {
2892
- if (__DEV__) {
2893
- if (
2894
- didWarnAboutUnmockedScheduler === false &&
2895
- Scheduler.unstable_flushAllWithoutAsserting === undefined
2896
- ) {
2897
- if (fiber.mode & ConcurrentMode) {
2898
- didWarnAboutUnmockedScheduler = true;
2899
- console.error(
2900
- 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
2901
- 'to guarantee consistent behaviour across tests and browsers. ' +
2902
- 'For example, with jest: \n' +
2903
- // Break up requires to avoid accidentally parsing them as dependencies.
2904
- "jest.mock('scheduler', () => require" +
2905
- "('scheduler/unstable_mock'));\n\n" +
2906
- 'For more info, visit https://reactjs.org/link/mock-scheduler',
2907
- );
2908
- } else if (warnAboutUnmockedScheduler === true) {
2909
- didWarnAboutUnmockedScheduler = true;
2910
- console.error(
2911
- 'Starting from React v18, the "scheduler" module will need to be mocked ' +
2912
- 'to guarantee consistent behaviour across tests and browsers. ' +
2913
- 'For example, with jest: \n' +
2914
- // Break up requires to avoid accidentally parsing them as dependencies.
2915
- "jest.mock('scheduler', () => require" +
2916
- "('scheduler/unstable_mock'));\n\n" +
2917
- 'For more info, visit https://reactjs.org/link/mock-scheduler',
2918
- );
2919
- }
2920
- }
2921
- }
2922
-}
2923
-
2924
-// `act` testing API
2925
-//
2926
-// TODO: This is mostly a copy-paste from the legacy `act`, which does not have
2927
-// access to the same internals that we do here. Some trade offs in the
2928
-// implementation no longer make sense.
2929
-
2930
-let isFlushingAct = false;
2931
-let isInsideThisAct = false;
2932
-
2933
-function shouldForceFlushFallbacksInDEV() {
2934
- // Never force flush in production. This function should get stripped out.
2935
- return __DEV__ && actingUpdatesScopeDepth > 0;
2936
-}
2937
-
2938
-const flushMockScheduler = Scheduler.unstable_flushAllWithoutAsserting;
2939
-const isSchedulerMocked = typeof flushMockScheduler === 'function';
2940
-
2941
-// Returns whether additional work was scheduled. Caller should keep flushing
2942
-// until there's no work left.
2943
-function flushActWork(): boolean {
2944
- if (flushMockScheduler !== undefined) {
2945
- const prevIsFlushing = isFlushingAct;
2946
- isFlushingAct = true;
2947
- try {
2948
- return flushMockScheduler();
2949
- } finally {
2950
- isFlushingAct = prevIsFlushing;
2951
- }
2952
- } else {
2953
- // No mock scheduler available. However, the only type of pending work is
2954
- // passive effects, which we control. So we can flush that.
2955
- const prevIsFlushing = isFlushingAct;
2956
- isFlushingAct = true;
2957
- try {
2958
- let didFlushWork = false;
2959
- while (flushPassiveEffects()) {
2960
- didFlushWork = true;
2961
- }
2962
- return didFlushWork;
2963
- } finally {
2964
- isFlushingAct = prevIsFlushing;
2965
- }
2966
- }
2967
-}
2968
-
2969
-function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
2970
- try {
2971
- flushActWork();
2972
- enqueueTask(() => {
2973
- if (flushActWork()) {
2974
- flushWorkAndMicroTasks(onDone);
2975
- } else {
2976
- onDone();
2977
- }
2978
- });
2979
- } catch (err) {
2980
- onDone(err);
2981
- }
2982
-}
2983
-
2984
-// we track the 'depth' of the act() calls with this counter,
2985
-// so we can tell if any async act() calls try to run in parallel.
2986
-
2987
-let actingUpdatesScopeDepth = 0;
2988
-
2989
-export function act(callback: () => Thenable<mixed>): Thenable<void> {
2990
- if (!__DEV__) {
2991
- invariant(
2992
- false,
2993
- 'act(...) is not supported in production builds of React.',
2994
- );
2995
- }
2996
-
2997
- const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
2998
- actingUpdatesScopeDepth++;
2999
-
3000
- const previousIsSomeRendererActing = IsSomeRendererActing.current;
3001
- const previousIsThisRendererActing = IsThisRendererActing.current;
3002
- const previousIsInsideThisAct = isInsideThisAct;
3003
- IsSomeRendererActing.current = true;
3004
- IsThisRendererActing.current = true;
3005
- isInsideThisAct = true;
3006
-
3007
- function onDone() {
3008
- actingUpdatesScopeDepth--;
3009
- IsSomeRendererActing.current = previousIsSomeRendererActing;
3010
- IsThisRendererActing.current = previousIsThisRendererActing;
3011
- isInsideThisAct = previousIsInsideThisAct;
3012
- if (__DEV__) {
3013
- if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
3014
- // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
3015
- console.error(
3016
- 'You seem to have overlapping act() calls, this is not supported. ' +
3017
- 'Be sure to await previous act() calls before making a new one. ',
3018
- );
3019
- }
3020
- }
3021
- }
3022
-
3023
- let result;
3024
- try {
3025
- result = batchedUpdates(callback);
3026
- } catch (error) {
3027
- // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth
3028
- onDone();
3029
- throw error;
3030
- }
3031
-
3032
- if (
3033
- result !== null &&
3034
- typeof result === 'object' &&
3035
- typeof result.then === 'function'
3036
- ) {
3037
- // setup a boolean that gets set to true only
3038
- // once this act() call is await-ed
3039
- let called = false;
3040
- if (__DEV__) {
3041
- if (typeof Promise !== 'undefined') {
3042
- //eslint-disable-next-line no-undef
3043
- Promise.resolve()
3044
- .then(() => {})
3045
- .then(() => {
3046
- if (called === false) {
3047
- console.error(
3048
- 'You called act(async () => ...) without await. ' +
3049
- 'This could lead to unexpected testing behaviour, interleaving multiple act ' +
3050
- 'calls and mixing their scopes. You should - await act(async () => ...);',
3051
- );
3052
- }
3053
- });
3054
- }
3055
- }
3056
-
3057
- // in the async case, the returned thenable runs the callback, flushes
3058
- // effects and microtasks in a loop until flushPassiveEffects() === false,
3059
- // and cleans up
3060
- return {
3061
- then(resolve, reject) {
3062
- called = true;
3063
- result.then(
3064
- () => {
3065
- if (
3066
- actingUpdatesScopeDepth > 1 ||
3067
- (isSchedulerMocked === true &&
3068
- previousIsSomeRendererActing === true)
3069
- ) {
3070
- onDone();
3071
- resolve();
3072
- return;
3073
- }
3074
- // we're about to exit the act() scope,
3075
- // now's the time to flush tasks/effects
3076
- flushWorkAndMicroTasks((err: ?Error) => {
3077
- onDone();
3078
- if (err) {
3079
- reject(err);
3080
- } else {
3081
- resolve();
3082
- }
3083
- });
3084
- },
3085
- err => {
3086
- onDone();
3087
- reject(err);
3088
- },
3089
- );
3090
- },
3091
- };
3092
- } else {
3093
- if (__DEV__) {
3094
- if (result !== undefined) {
3095
- console.error(
3096
- 'The callback passed to act(...) function ' +
3097
- 'must return undefined, or a Promise. You returned %s',
3098
- result,
3099
- );
3100
- }
3101
- }
3102
-
3103
- // flush effects until none remain, and cleanup
3104
- try {
3105
- if (
3106
- actingUpdatesScopeDepth === 1 &&
3107
- (isSchedulerMocked === false || previousIsSomeRendererActing === false)
3108
- ) {
3109
- // we're about to exit the act() scope,
3110
- // now's the time to flush effects
3111
- flushActWork();
3112
- }
3113
- onDone();
3114
- } catch (err) {
3115
- onDone();
3116
- throw err;
3117
- }
3118
-
3119
- // in the sync case, the returned thenable only warns *if* await-ed
3120
- return {
3121
- then(resolve) {
3122
- if (__DEV__) {
3123
- console.error(
3124
- 'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
3125
- );
3126
- }
3127
- resolve();
3128
- },
3129
- };
3130
- }
3131
-}