main
js 738 lines 27 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {FiberRoot} from './ReactInternalTypes';
11 import type {Lane, Lanes} from './ReactFiberLane';
12 import type {PriorityLevel} from 'scheduler/src/SchedulerPriorities';
13 import type {Transition} from 'react/src/ReactStartTransition';
14
15 import {
16 disableLegacyMode,
17 disableSchedulerTimeoutInWorkLoop,
18 enableProfilerTimer,
19 enableProfilerNestedUpdatePhase,
20 enableComponentPerformanceTrack,
21 enableYieldingBeforePassive,
22 enableGestureTransition,
23 enableDefaultTransitionIndicator,
24 } from 'shared/ReactFeatureFlags';
25 import {
26 NoLane,
27 NoLanes,
28 SyncLane,
29 DefaultLane,
30 getHighestPriorityLane,
31 getNextLanes,
32 includesSyncLane,
33 markStarvedLanesAsExpired,
34 claimNextTransitionUpdateLane,
35 getNextLanesToFlushSync,
36 checkIfRootIsPrerendering,
37 isGestureRender,
38 } from './ReactFiberLane';
39 import {
40 CommitContext,
41 NoContext,
42 RenderContext,
43 flushPendingEffects,
44 flushPendingEffectsDelayed,
45 getExecutionContext,
46 getWorkInProgressRoot,
47 getWorkInProgressRootRenderLanes,
48 getRootWithPendingPassiveEffects,
49 getPendingPassiveEffectsLanes,
50 hasPendingCommitEffects,
51 isWorkLoopSuspendedOnData,
52 performWorkOnRoot,
53 } from './ReactFiberWorkLoop';
54 import {LegacyRoot} from './ReactRootTags';
55 import {
56 ImmediatePriority as ImmediateSchedulerPriority,
57 UserBlockingPriority as UserBlockingSchedulerPriority,
58 NormalPriority as NormalSchedulerPriority,
59 IdlePriority as IdleSchedulerPriority,
60 cancelCallback as Scheduler_cancelCallback,
61 scheduleCallback as Scheduler_scheduleCallback,
62 now,
63 } from './Scheduler';
64 import {
65 DiscreteEventPriority,
66 ContinuousEventPriority,
67 DefaultEventPriority,
68 IdleEventPriority,
69 lanesToEventPriority,
70 } from './ReactEventPriorities';
71 import {
72 supportsMicrotasks,
73 scheduleMicrotask,
74 shouldAttemptEagerTransition,
75 trackSchedulerEvent,
76 noTimeout,
77 } from './ReactFiberConfig';
78
79 import ReactSharedInternals from 'shared/ReactSharedInternals';
80 import {
81 resetNestedUpdateFlag,
82 syncNestedUpdateFlag,
83 } from './ReactProfilerTimer';
84 import {peekEntangledActionLane} from './ReactFiberAsyncAction';
85
86 import noop from 'shared/noop';
87 import reportGlobalError from 'shared/reportGlobalError';
88
89 import {
90 startIsomorphicDefaultIndicatorIfNeeded,
91 hasOngoingIsomorphicIndicator,
92 retainIsomorphicIndicator,
93 markIsomorphicIndicatorHandled,
94 } from './ReactFiberAsyncAction';
95
96 // A linked list of all the roots with pending work. In an idiomatic app,
97 // there's only a single root, but we do support multi root apps, hence this
98 // extra complexity. But this module is optimized for the single root case.
99 export let firstScheduledRoot: FiberRoot | null = null;
100 let lastScheduledRoot: FiberRoot | null = null;
101
102 // Used to prevent redundant mircotasks from being scheduled.
103 let didScheduleMicrotask: boolean = false;
104 // `act` "microtasks" are scheduled on the `act` queue instead of an actual
105 // microtask, so we have to dedupe those separately. This wouldn't be an issue
106 // if we required all `act` calls to be awaited, which we might in the future.
107 let didScheduleMicrotask_act: boolean = false;
108
109 // Used to quickly bail out of flushSync if there's no sync work to do.
110 let mightHavePendingSyncWork: boolean = false;
111
112 let isFlushingWork: boolean = false;
113
114 let currentEventTransitionLane: Lane = NoLane;
115
116 export function ensureRootIsScheduled(root: FiberRoot): void {
117 // This function is called whenever a root receives an update. It does two
118 // things 1) it ensures the root is in the root schedule, and 2) it ensures
119 // there's a pending microtask to process the root schedule.
120 //
121 // Most of the actual scheduling logic does not happen until
122 // `scheduleTaskForRootDuringMicrotask` runs.
123
124 // Add the root to the schedule
125 if (root === lastScheduledRoot || root.next !== null) {
126 // Fast path. This root is already scheduled.
127 } else {
128 if (lastScheduledRoot === null) {
129 firstScheduledRoot = lastScheduledRoot = root;
130 } else {
131 lastScheduledRoot.next = root;
132 lastScheduledRoot = root;
133 }
134 }
135
136 // Any time a root received an update, we set this to true until the next time
137 // we process the schedule. If it's false, then we can quickly exit flushSync
138 // without consulting the schedule.
139 mightHavePendingSyncWork = true;
140
141 ensureScheduleIsScheduled();
142
143 if (
144 __DEV__ &&
145 !disableLegacyMode &&
146 ReactSharedInternals.isBatchingLegacy &&
147 root.tag === LegacyRoot
148 ) {
149 // Special `act` case: Record whenever a legacy update is scheduled.
150 ReactSharedInternals.didScheduleLegacyUpdate = true;
151 }
152 }
153
154 export function ensureScheduleIsScheduled(): void {
155 // At the end of the current event, go through each of the roots and ensure
156 // there's a task scheduled for each one at the correct priority.
157 if (__DEV__ && ReactSharedInternals.actQueue !== null) {
158 // We're inside an `act` scope.
159 if (!didScheduleMicrotask_act) {
160 didScheduleMicrotask_act = true;
161 scheduleImmediateRootScheduleTask();
162 }
163 } else {
164 if (!didScheduleMicrotask) {
165 didScheduleMicrotask = true;
166 scheduleImmediateRootScheduleTask();
167 }
168 }
169 }
170
171 export function flushSyncWorkOnAllRoots() {
172 // This is allowed to be called synchronously, but the caller should check
173 // the execution context first.
174 flushSyncWorkAcrossRoots_impl(NoLanes, false);
175 }
176
177 export function flushSyncWorkOnLegacyRootsOnly() {
178 // This is allowed to be called synchronously, but the caller should check
179 // the execution context first.
180 if (!disableLegacyMode) {
181 flushSyncWorkAcrossRoots_impl(NoLanes, true);
182 }
183 }
184
185 function flushSyncWorkAcrossRoots_impl(
186 syncTransitionLanes: Lanes | Lane,
187 onlyLegacy: boolean,
188 ) {
189 if (isFlushingWork) {
190 // Prevent reentrancy.
191 // TODO: Is this overly defensive? The callers must check the execution
192 // context first regardless.
193 return;
194 }
195
196 if (!mightHavePendingSyncWork) {
197 // Fast path. There's no sync work to do.
198 return;
199 }
200
201 // There may or may not be synchronous work scheduled. Let's check.
202 let didPerformSomeWork;
203 isFlushingWork = true;
204 do {
205 didPerformSomeWork = false;
206 let root = firstScheduledRoot;
207 while (root !== null) {
208 if (onlyLegacy && (disableLegacyMode || root.tag !== LegacyRoot)) {
209 // Skip non-legacy roots.
210 } else {
211 if (syncTransitionLanes !== NoLanes) {
212 const nextLanes = getNextLanesToFlushSync(root, syncTransitionLanes);
213 if (nextLanes !== NoLanes) {
214 // This root has pending sync work. Flush it now.
215 didPerformSomeWork = true;
216 performSyncWorkOnRoot(root, nextLanes);
217 }
218 } else {
219 const workInProgressRoot = getWorkInProgressRoot();
220 const workInProgressRootRenderLanes =
221 getWorkInProgressRootRenderLanes();
222 const rootHasPendingCommit =
223 root.cancelPendingCommit !== null ||
224 root.timeoutHandle !== noTimeout;
225 const nextLanes = getNextLanes(
226 root,
227 root === workInProgressRoot
228 ? workInProgressRootRenderLanes
229 : NoLanes,
230 rootHasPendingCommit,
231 );
232 if (
233 (includesSyncLane(nextLanes) ||
234 (enableGestureTransition && isGestureRender(nextLanes))) &&
235 !checkIfRootIsPrerendering(root, nextLanes)
236 ) {
237 // This root has pending sync work. Flush it now.
238 didPerformSomeWork = true;
239 performSyncWorkOnRoot(root, nextLanes);
240 }
241 }
242 }
243 root = root.next;
244 }
245 } while (didPerformSomeWork);
246 isFlushingWork = false;
247 }
248
249 function processRootScheduleInImmediateTask() {
250 if (enableProfilerTimer && enableComponentPerformanceTrack) {
251 // Track the currently executing event if there is one so we can ignore this
252 // event when logging events.
253 trackSchedulerEvent();
254 }
255
256 processRootScheduleInMicrotask();
257 }
258
259 function processRootScheduleInMicrotask() {
260 // This function is always called inside a microtask. It should never be
261 // called synchronously.
262 didScheduleMicrotask = false;
263 if (__DEV__) {
264 didScheduleMicrotask_act = false;
265 }
266
267 // We'll recompute this as we iterate through all the roots and schedule them.
268 mightHavePendingSyncWork = false;
269
270 let syncTransitionLanes = NoLanes;
271 if (currentEventTransitionLane !== NoLane) {
272 if (shouldAttemptEagerTransition()) {
273 // A transition was scheduled during an event, but we're going to try to
274 // render it synchronously anyway. We do this during a popstate event to
275 // preserve the scroll position of the previous page.
276 syncTransitionLanes = currentEventTransitionLane;
277 } else if (enableDefaultTransitionIndicator) {
278 // If we have a Transition scheduled by this event it might be paired
279 // with Default lane scheduled loading indicators. To unbatch it from
280 // other events later on, flush it early to determine whether it
281 // rendered an indicator. This ensures that setState in default priority
282 // event doesn't trigger onDefaultTransitionIndicator.
283 syncTransitionLanes = DefaultLane;
284 }
285 }
286
287 const currentTime = now();
288
289 let prev = null;
290 let root = firstScheduledRoot;
291 while (root !== null) {
292 const next = root.next;
293 const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
294 if (nextLanes === NoLane) {
295 // This root has no more pending work. Remove it from the schedule. To
296 // guard against subtle reentrancy bugs, this microtask is the only place
297 // we do this — you can add roots to the schedule whenever, but you can
298 // only remove them here.
299
300 // Null this out so we know it's been removed from the schedule.
301 root.next = null;
302 if (prev === null) {
303 // This is the new head of the list
304 firstScheduledRoot = next;
305 } else {
306 prev.next = next;
307 }
308 if (next === null) {
309 // This is the new tail of the list
310 lastScheduledRoot = prev;
311 }
312 } else {
313 // This root still has work. Keep it in the list.
314 prev = root;
315
316 // This is a fast-path optimization to early exit from
317 // flushSyncWorkOnAllRoots if we can be certain that there is no remaining
318 // synchronous work to perform. Set this to true if there might be sync
319 // work left.
320 if (
321 // Skip the optimization if syncTransitionLanes is set
322 syncTransitionLanes !== NoLanes ||
323 // Common case: we're not treating any extra lanes as synchronous, so we
324 // can just check if the next lanes are sync.
325 includesSyncLane(nextLanes) ||
326 (enableGestureTransition && isGestureRender(nextLanes))
327 ) {
328 mightHavePendingSyncWork = true;
329 }
330 }
331 root = next;
332 }
333
334 // At the end of the microtask, flush any pending synchronous work. This has
335 // to come at the end, because it does actual rendering work that might throw.
336 // If we're in the middle of a View Transition async sequence, we don't want to
337 // interrupt that sequence. Instead, we'll flush any remaining work when it
338 // completes.
339 if (!hasPendingCommitEffects()) {
340 flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
341 }
342
343 if (currentEventTransitionLane !== NoLane) {
344 // Reset Event Transition Lane so that we allocate a new one next time.
345 currentEventTransitionLane = NoLane;
346 startDefaultTransitionIndicatorIfNeeded();
347 }
348 }
349
350 function startDefaultTransitionIndicatorIfNeeded() {
351 if (!enableDefaultTransitionIndicator) {
352 return;
353 }
354 // Check if we need to start an isomorphic indicator like if an async action
355 // was started.
356 startIsomorphicDefaultIndicatorIfNeeded();
357 // Check all the roots if there are any new indicators needed.
358 let root = firstScheduledRoot;
359 while (root !== null) {
360 if (root.indicatorLanes !== NoLanes && root.pendingIndicator === null) {
361 // We have new indicator lanes that requires a loading state. Start the
362 // default transition indicator.
363 if (hasOngoingIsomorphicIndicator()) {
364 // We already have an isomorphic indicator going which means it has to
365 // also apply to this root since it implies all roots have the same one.
366 // We retain this indicator so that it keeps going until we commit this
367 // root.
368 root.pendingIndicator = retainIsomorphicIndicator();
369 } else {
370 try {
371 const onDefaultTransitionIndicator =
372 root.onDefaultTransitionIndicator;
373 root.pendingIndicator = onDefaultTransitionIndicator() || noop;
374 } catch (x) {
375 root.pendingIndicator = noop;
376 reportGlobalError(x);
377 }
378 }
379 }
380 root = root.next;
381 }
382 }
383
384 function scheduleTaskForRootDuringMicrotask(
385 root: FiberRoot,
386 currentTime: number,
387 ): Lane {
388 // This function is always called inside a microtask, or at the very end of a
389 // rendering task right before we yield to the main thread. It should never be
390 // called synchronously.
391
392 // This function also never performs React work synchronously; it should
393 // only schedule work to be performed later, in a separate task or microtask.
394
395 // Check if any lanes are being starved by other work. If so, mark them as
396 // expired so we know to work on those next.
397 markStarvedLanesAsExpired(root, currentTime);
398
399 // Determine the next lanes to work on, and their priority.
400 const rootWithPendingPassiveEffects = getRootWithPendingPassiveEffects();
401 const pendingPassiveEffectsLanes = getPendingPassiveEffectsLanes();
402 const workInProgressRoot = getWorkInProgressRoot();
403 const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
404 const rootHasPendingCommit =
405 root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout;
406 const nextLanes =
407 enableYieldingBeforePassive && root === rootWithPendingPassiveEffects
408 ? // This will schedule the callback at the priority of the lane but we used to
409 // always schedule it at NormalPriority. Discrete will flush it sync anyway.
410 // So the only difference is Idle and it doesn't seem necessarily right for that
411 // to get upgraded beyond something important just because we're past commit.
412 pendingPassiveEffectsLanes
413 : getNextLanes(
414 root,
415 root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
416 rootHasPendingCommit,
417 );
418
419 const existingCallbackNode = root.callbackNode;
420 if (
421 // Check if there's nothing to work on
422 nextLanes === NoLanes ||
423 // If this root is currently suspended and waiting for data to resolve, don't
424 // schedule a task to render it. We'll either wait for a ping, or wait to
425 // receive an update.
426 //
427 // Suspended render phase
428 (root === workInProgressRoot && isWorkLoopSuspendedOnData()) ||
429 // Suspended commit phase
430 root.cancelPendingCommit !== null
431 ) {
432 // Fast path: There's nothing to work on.
433 if (existingCallbackNode !== null) {
434 cancelCallback(existingCallbackNode);
435 }
436 root.callbackNode = null;
437 root.callbackPriority = NoLane;
438 return NoLane;
439 }
440
441 // Schedule a new callback in the host environment.
442 if (
443 includesSyncLane(nextLanes) &&
444 // If we're prerendering, then we should use the concurrent work loop
445 // even if the lanes are synchronous, so that prerendering never blocks
446 // the main thread.
447 !checkIfRootIsPrerendering(root, nextLanes)
448 ) {
449 // Synchronous work is always flushed at the end of the microtask, so we
450 // don't need to schedule an additional task.
451 if (existingCallbackNode !== null) {
452 cancelCallback(existingCallbackNode);
453 }
454 root.callbackPriority = SyncLane;
455 root.callbackNode = null;
456 return SyncLane;
457 } else {
458 // We use the highest priority lane to represent the priority of the callback.
459 const existingCallbackPriority = root.callbackPriority;
460 const newCallbackPriority = getHighestPriorityLane(nextLanes);
461
462 if (
463 newCallbackPriority === existingCallbackPriority &&
464 // Special case related to `act`. If the currently scheduled task is a
465 // Scheduler task, rather than an `act` task, cancel it and re-schedule
466 // on the `act` queue.
467 !(
468 __DEV__ &&
469 ReactSharedInternals.actQueue !== null &&
470 existingCallbackNode !== fakeActCallbackNode
471 )
472 ) {
473 // The priority hasn't changed. We can reuse the existing task.
474 return newCallbackPriority;
475 } else {
476 // Cancel the existing callback. We'll schedule a new one below.
477 cancelCallback(existingCallbackNode);
478 }
479
480 let schedulerPriorityLevel;
481 switch (lanesToEventPriority(nextLanes)) {
482 // Scheduler does have an "ImmediatePriority", but now that we use
483 // microtasks for sync work we no longer use that. Any sync work that
484 // reaches this path is meant to be time sliced.
485 case DiscreteEventPriority:
486 case ContinuousEventPriority:
487 schedulerPriorityLevel = UserBlockingSchedulerPriority;
488 break;
489 case DefaultEventPriority:
490 schedulerPriorityLevel = NormalSchedulerPriority;
491 break;
492 case IdleEventPriority:
493 schedulerPriorityLevel = IdleSchedulerPriority;
494 break;
495 default:
496 schedulerPriorityLevel = NormalSchedulerPriority;
497 break;
498 }
499
500 const newCallbackNode = scheduleCallback(
501 schedulerPriorityLevel,
502 performWorkOnRootViaSchedulerTask.bind(null, root),
503 );
504
505 root.callbackPriority = newCallbackPriority;
506 root.callbackNode = newCallbackNode;
507 return newCallbackPriority;
508 }
509 }
510
511 type RenderTaskFn = (didTimeout: boolean) => RenderTaskFn | null;
512
513 function performWorkOnRootViaSchedulerTask(
514 root: FiberRoot,
515 didTimeout: boolean,
516 ): RenderTaskFn | null {
517 // This is the entry point for concurrent tasks scheduled via Scheduler (and
518 // postTask, in the future).
519
520 if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
521 resetNestedUpdateFlag();
522 }
523
524 if (enableProfilerTimer && enableComponentPerformanceTrack) {
525 // Track the currently executing event if there is one so we can ignore this
526 // event when logging events.
527 trackSchedulerEvent();
528 }
529
530 if (hasPendingCommitEffects()) {
531 // We are currently in the middle of an async committing (such as a View Transition).
532 // We could force these to flush eagerly but it's better to defer any work until
533 // it finishes. This may not be the same root as we're waiting on.
534 // TODO: This relies on the commit eventually calling ensureRootIsScheduled which
535 // always calls processRootScheduleInMicrotask which in turn always loops through
536 // all the roots to figure out. This is all a bit inefficient and if optimized
537 // it'll need to consider rescheduling a task for any skipped roots.
538 root.callbackNode = null;
539 root.callbackPriority = NoLane;
540 return null;
541 }
542
543 // Flush any pending passive effects before deciding which lanes to work on,
544 // in case they schedule additional work.
545 const originalCallbackNode = root.callbackNode;
546 const didFlushPassiveEffects = flushPendingEffectsDelayed();
547 if (didFlushPassiveEffects) {
548 // Something in the passive effect phase may have canceled the current task.
549 // Check if the task node for this root was changed.
550 if (root.callbackNode !== originalCallbackNode) {
551 // The current task was canceled. Exit. We don't need to call
552 // `ensureRootIsScheduled` because the check above implies either that
553 // there's a new task, or that there's no remaining work on this root.
554 return null;
555 } else {
556 // Current task was not canceled. Continue.
557 }
558 }
559
560 // Determine the next lanes to work on, using the fields stored on the root.
561 // TODO: We already called getNextLanes when we scheduled the callback; we
562 // should be able to avoid calling it again by stashing the result on the
563 // root object. However, because we always schedule the callback during
564 // a microtask (scheduleTaskForRootDuringMicrotask), it's possible that
565 // an update was scheduled earlier during this same browser task (and
566 // therefore before the microtasks have run). That's because Scheduler batches
567 // together multiple callbacks into a single browser macrotask, without
568 // yielding to microtasks in between. We should probably change this to align
569 // with the postTask behavior (and literally use postTask when
570 // it's available).
571 const workInProgressRoot = getWorkInProgressRoot();
572 const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
573 const rootHasPendingCommit =
574 root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout;
575 const lanes = getNextLanes(
576 root,
577 root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
578 rootHasPendingCommit,
579 );
580 if (lanes === NoLanes) {
581 // No more work on this root.
582 return null;
583 }
584
585 // Enter the work loop.
586 // TODO: We only check `didTimeout` defensively, to account for a Scheduler
587 // bug we're still investigating. Once the bug in Scheduler is fixed,
588 // we can remove this, since we track expiration ourselves.
589 const forceSync = !disableSchedulerTimeoutInWorkLoop && didTimeout;
590 performWorkOnRoot(root, lanes, forceSync);
591
592 // The work loop yielded, but there may or may not be work left at the current
593 // priority. Need to determine whether we need to schedule a continuation.
594 // Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask;
595 // however, since most of the logic for determining if we need a continuation
596 // versus a new task is the same, we cheat a bit and call it here. This is
597 // only safe to do because we know we're at the end of the browser task.
598 // So although it's not an actual microtask, it might as well be.
599 scheduleTaskForRootDuringMicrotask(root, now());
600 if (root.callbackNode != null && root.callbackNode === originalCallbackNode) {
601 // The task node scheduled for this root is the same one that's
602 // currently executed. Need to return a continuation.
603 return performWorkOnRootViaSchedulerTask.bind(null, root);
604 }
605 return null;
606 }
607
608 function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes) {
609 // This is the entry point for synchronous tasks that don't go
610 // through Scheduler.
611 const didFlushPassiveEffects = flushPendingEffects();
612 if (didFlushPassiveEffects) {
613 // If passive effects were flushed, exit to the outer work loop in the root
614 // scheduler, so we can recompute the priority.
615 return null;
616 }
617 if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
618 syncNestedUpdateFlag();
619 }
620 const forceSync = true;
621 performWorkOnRoot(root, lanes, forceSync);
622 }
623
624 const fakeActCallbackNode = {};
625
626 function scheduleCallback(
627 priorityLevel: PriorityLevel,
628 callback: RenderTaskFn,
629 ) {
630 if (__DEV__ && ReactSharedInternals.actQueue !== null) {
631 // Special case: We're inside an `act` scope (a testing utility).
632 // Instead of scheduling work in the host environment, add it to a
633 // fake internal queue that's managed by the `act` implementation.
634 ReactSharedInternals.actQueue.push(callback);
635 return fakeActCallbackNode;
636 } else {
637 return Scheduler_scheduleCallback(priorityLevel, callback);
638 }
639 }
640
641 function cancelCallback(callbackNode: mixed) {
642 if (__DEV__ && callbackNode === fakeActCallbackNode) {
643 // Special `act` case: check if this is the fake callback node used by
644 // the `act` implementation.
645 } else if (callbackNode !== null) {
646 Scheduler_cancelCallback(callbackNode);
647 }
648 }
649
650 function scheduleImmediateRootScheduleTask() {
651 if (__DEV__ && ReactSharedInternals.actQueue !== null) {
652 // Special case: Inside an `act` scope, we push microtasks to the fake `act`
653 // callback queue. This is because we currently support calling `act`
654 // without awaiting the result. The plan is to deprecate that, and require
655 // that you always await the result so that the microtasks have a chance to
656 // run. But it hasn't happened yet.
657 ReactSharedInternals.actQueue.push(() => {
658 processRootScheduleInMicrotask();
659 return null;
660 });
661 }
662
663 // TODO: Can we land supportsMicrotasks? Which environments don't support it?
664 // Alternatively, can we move this check to the host config?
665 // $FlowFixMe[constant-condition]
666 if (supportsMicrotasks) {
667 scheduleMicrotask(() => {
668 // In Safari, appending an iframe forces microtasks to run.
669 // https://github.com/facebook/react/issues/22459
670 // We don't support running callbacks in the middle of render
671 // or commit so we need to check against that.
672 const executionContext = getExecutionContext();
673 if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
674 // Note that this would still prematurely flush the callbacks
675 // if this happens outside render or commit phase (e.g. in an event).
676
677 // Intentionally using a macrotask instead of a microtask here. This is
678 // wrong semantically but it prevents an infinite loop. The bug is
679 // Safari's, not ours, so we just do our best to not crash even though
680 // the behavior isn't completely correct.
681 Scheduler_scheduleCallback(
682 ImmediateSchedulerPriority,
683 processRootScheduleInImmediateTask,
684 );
685 return;
686 }
687 processRootScheduleInMicrotask();
688 });
689 } else {
690 // If microtasks are not supported, use Scheduler.
691 Scheduler_scheduleCallback(
692 ImmediateSchedulerPriority,
693 processRootScheduleInImmediateTask,
694 );
695 }
696 }
697
698 export function requestTransitionLane(
699 // This argument isn't used, it's only here to encourage the caller to
700 // check that it's inside a transition before calling this function.
701 // TODO: Make this non-nullable. Requires a tweak to useOptimistic.
702 transition: Transition | null,
703 ): Lane {
704 // The algorithm for assigning an update to a lane should be stable for all
705 // updates at the same priority within the same event. To do this, the
706 // inputs to the algorithm must be the same.
707 //
708 // The trick we use is to cache the first of each of these inputs within an
709 // event. Then reset the cached values once we can be sure the event is
710 // over. Our heuristic for that is whenever we enter a concurrent work loop.
711 if (currentEventTransitionLane === NoLane) {
712 // All transitions within the same event are assigned the same lane.
713 const actionScopeLane = peekEntangledActionLane();
714 currentEventTransitionLane =
715 actionScopeLane !== NoLane
716 ? // We're inside an async action scope. Reuse the same lane.
717 actionScopeLane
718 : // We may or may not be inside an async action scope. If we are, this
719 // is the first update in that scope. Either way, we need to get a
720 // fresh transition lane.
721 claimNextTransitionUpdateLane();
722 }
723 return currentEventTransitionLane;
724 }
725
726 export function didCurrentEventScheduleTransition(): boolean {
727 return currentEventTransitionLane !== NoLane;
728 }
729
730 export function markIndicatorHandled(root: FiberRoot): void {
731 if (enableDefaultTransitionIndicator) {
732 // The current transition event rendered a synchronous loading state.
733 // Clear it from the indicator lanes. We don't need to show a separate
734 // loading state for this lane.
735 root.indicatorLanes &= ~currentEventTransitionLane;
736 markIsomorphicIndicatorHandled();
737 }
738 }