mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
c3345638cb
Before this change, `useFormStatus` is only activated if a form is
submitted by an action function (either `<form action={actionFn}>` or
`<button formAction={actionFn}>`).
After this change, `useFormStatus` will also be activated if you call
`startTransition(actionFn)` inside a submit event handler that is
`preventDefault`-ed.
This is the last missing piece for implementing a custom `action` prop
that is progressively enhanced using `onSubmit` while maintaining the
same behavior as built-in form actions.
Here's the basic recipe for implementing a progressively-enhanced form
action. This would typically be implemented in your UI component
library, not regular application code:
```js
import {requestFormReset} from 'react-dom';
// To implement progressive enhancement, pass both a form action *and* a
// submit event handler. The action is used for submissions that happen
// before hydration, and the submit handler is used for submissions that
// happen after.
<form
action={action}
onSubmit={(event) => {
// After hydration, we upgrade the form with additional client-
// only behavior.
event.preventDefault();
// Manually dispatch the action.
startTransition(async () => {
// (Optional) Reset any uncontrolled inputs once the action is
// complete, like built-in form actions do.
requestFormReset(event.target);
// ...Do extra action-y stuff in here, like setting a custom
// optimistic state...
// Call the user-provided action
const formData = new FormData(event.target);
await action(formData);
});
}}
/>
```
489 lines
17 KiB
JavaScript
489 lines
17 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 {FiberRoot} from './ReactInternalTypes';
|
|
import type {Lane} from './ReactFiberLane';
|
|
import type {PriorityLevel} from 'scheduler/src/SchedulerPriorities';
|
|
import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent';
|
|
|
|
import {
|
|
disableLegacyMode,
|
|
enableDeferRootSchedulingToMicrotask,
|
|
} from 'shared/ReactFeatureFlags';
|
|
import {
|
|
NoLane,
|
|
NoLanes,
|
|
SyncLane,
|
|
getHighestPriorityLane,
|
|
getNextLanes,
|
|
includesSyncLane,
|
|
markStarvedLanesAsExpired,
|
|
upgradePendingLaneToSync,
|
|
claimNextTransitionLane,
|
|
} from './ReactFiberLane';
|
|
import {
|
|
CommitContext,
|
|
NoContext,
|
|
RenderContext,
|
|
getExecutionContext,
|
|
getWorkInProgressRoot,
|
|
getWorkInProgressRootRenderLanes,
|
|
isWorkLoopSuspendedOnData,
|
|
performConcurrentWorkOnRoot,
|
|
performSyncWorkOnRoot,
|
|
} from './ReactFiberWorkLoop';
|
|
import {LegacyRoot} from './ReactRootTags';
|
|
import {
|
|
ImmediatePriority as ImmediateSchedulerPriority,
|
|
UserBlockingPriority as UserBlockingSchedulerPriority,
|
|
NormalPriority as NormalSchedulerPriority,
|
|
IdlePriority as IdleSchedulerPriority,
|
|
cancelCallback as Scheduler_cancelCallback,
|
|
scheduleCallback as Scheduler_scheduleCallback,
|
|
now,
|
|
} from './Scheduler';
|
|
import {
|
|
DiscreteEventPriority,
|
|
ContinuousEventPriority,
|
|
DefaultEventPriority,
|
|
IdleEventPriority,
|
|
lanesToEventPriority,
|
|
} from './ReactEventPriorities';
|
|
import {
|
|
supportsMicrotasks,
|
|
scheduleMicrotask,
|
|
shouldAttemptEagerTransition,
|
|
} from './ReactFiberConfig';
|
|
|
|
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
|
|
|
// A linked list of all the roots with pending work. In an idiomatic app,
|
|
// there's only a single root, but we do support multi root apps, hence this
|
|
// extra complexity. But this module is optimized for the single root case.
|
|
let firstScheduledRoot: FiberRoot | null = null;
|
|
let lastScheduledRoot: FiberRoot | null = null;
|
|
|
|
// Used to prevent redundant mircotasks from being scheduled.
|
|
let didScheduleMicrotask: boolean = false;
|
|
// `act` "microtasks" are scheduled on the `act` queue instead of an actual
|
|
// microtask, so we have to dedupe those separately. This wouldn't be an issue
|
|
// if we required all `act` calls to be awaited, which we might in the future.
|
|
let didScheduleMicrotask_act: boolean = false;
|
|
|
|
// Used to quickly bail out of flushSync if there's no sync work to do.
|
|
let mightHavePendingSyncWork: boolean = false;
|
|
|
|
let isFlushingWork: boolean = false;
|
|
|
|
let currentEventTransitionLane: Lane = NoLane;
|
|
|
|
export function ensureRootIsScheduled(root: FiberRoot): void {
|
|
// This function is called whenever a root receives an update. It does two
|
|
// things 1) it ensures the root is in the root schedule, and 2) it ensures
|
|
// there's a pending microtask to process the root schedule.
|
|
//
|
|
// Most of the actual scheduling logic does not happen until
|
|
// `scheduleTaskForRootDuringMicrotask` runs.
|
|
|
|
// Add the root to the schedule
|
|
if (root === lastScheduledRoot || root.next !== null) {
|
|
// Fast path. This root is already scheduled.
|
|
} else {
|
|
if (lastScheduledRoot === null) {
|
|
firstScheduledRoot = lastScheduledRoot = root;
|
|
} else {
|
|
lastScheduledRoot.next = root;
|
|
lastScheduledRoot = root;
|
|
}
|
|
}
|
|
|
|
// Any time a root received an update, we set this to true until the next time
|
|
// we process the schedule. If it's false, then we can quickly exit flushSync
|
|
// without consulting the schedule.
|
|
mightHavePendingSyncWork = true;
|
|
|
|
// At the end of the current event, go through each of the roots and ensure
|
|
// there's a task scheduled for each one at the correct priority.
|
|
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
|
|
// We're inside an `act` scope.
|
|
if (!didScheduleMicrotask_act) {
|
|
didScheduleMicrotask_act = true;
|
|
scheduleImmediateTask(processRootScheduleInMicrotask);
|
|
}
|
|
} else {
|
|
if (!didScheduleMicrotask) {
|
|
didScheduleMicrotask = true;
|
|
scheduleImmediateTask(processRootScheduleInMicrotask);
|
|
}
|
|
}
|
|
|
|
if (!enableDeferRootSchedulingToMicrotask) {
|
|
// While this flag is disabled, we schedule the render task immediately
|
|
// instead of waiting a microtask.
|
|
// TODO: We need to land enableDeferRootSchedulingToMicrotask ASAP to
|
|
// unblock additional features we have planned.
|
|
scheduleTaskForRootDuringMicrotask(root, now());
|
|
}
|
|
|
|
if (
|
|
__DEV__ &&
|
|
!disableLegacyMode &&
|
|
ReactSharedInternals.isBatchingLegacy &&
|
|
root.tag === LegacyRoot
|
|
) {
|
|
// Special `act` case: Record whenever a legacy update is scheduled.
|
|
ReactSharedInternals.didScheduleLegacyUpdate = true;
|
|
}
|
|
}
|
|
|
|
export function flushSyncWorkOnAllRoots() {
|
|
// This is allowed to be called synchronously, but the caller should check
|
|
// the execution context first.
|
|
flushSyncWorkAcrossRoots_impl(false);
|
|
}
|
|
|
|
export function flushSyncWorkOnLegacyRootsOnly() {
|
|
// This is allowed to be called synchronously, but the caller should check
|
|
// the execution context first.
|
|
if (!disableLegacyMode) {
|
|
flushSyncWorkAcrossRoots_impl(true);
|
|
}
|
|
}
|
|
|
|
function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
|
|
if (isFlushingWork) {
|
|
// Prevent reentrancy.
|
|
// TODO: Is this overly defensive? The callers must check the execution
|
|
// context first regardless.
|
|
return;
|
|
}
|
|
|
|
if (!mightHavePendingSyncWork) {
|
|
// Fast path. There's no sync work to do.
|
|
return;
|
|
}
|
|
|
|
// There may or may not be synchronous work scheduled. Let's check.
|
|
let didPerformSomeWork;
|
|
isFlushingWork = true;
|
|
do {
|
|
didPerformSomeWork = false;
|
|
let root = firstScheduledRoot;
|
|
while (root !== null) {
|
|
if (onlyLegacy && (disableLegacyMode || root.tag !== LegacyRoot)) {
|
|
// Skip non-legacy roots.
|
|
} else {
|
|
const workInProgressRoot = getWorkInProgressRoot();
|
|
const workInProgressRootRenderLanes =
|
|
getWorkInProgressRootRenderLanes();
|
|
const nextLanes = getNextLanes(
|
|
root,
|
|
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
|
|
);
|
|
if (includesSyncLane(nextLanes)) {
|
|
// This root has pending sync work. Flush it now.
|
|
didPerformSomeWork = true;
|
|
performSyncWorkOnRoot(root, nextLanes);
|
|
}
|
|
}
|
|
root = root.next;
|
|
}
|
|
} while (didPerformSomeWork);
|
|
isFlushingWork = false;
|
|
}
|
|
|
|
function processRootScheduleInMicrotask() {
|
|
// This function is always called inside a microtask. It should never be
|
|
// called synchronously.
|
|
didScheduleMicrotask = false;
|
|
if (__DEV__) {
|
|
didScheduleMicrotask_act = false;
|
|
}
|
|
|
|
// We'll recompute this as we iterate through all the roots and schedule them.
|
|
mightHavePendingSyncWork = false;
|
|
|
|
const currentTime = now();
|
|
|
|
let prev = null;
|
|
let root = firstScheduledRoot;
|
|
while (root !== null) {
|
|
const next = root.next;
|
|
|
|
if (
|
|
currentEventTransitionLane !== NoLane &&
|
|
shouldAttemptEagerTransition()
|
|
) {
|
|
// A transition was scheduled during an event, but we're going to try to
|
|
// render it synchronously anyway. We do this during a popstate event to
|
|
// preserve the scroll position of the previous page.
|
|
upgradePendingLaneToSync(root, currentEventTransitionLane);
|
|
}
|
|
|
|
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
|
|
if (nextLanes === NoLane) {
|
|
// This root has no more pending work. Remove it from the schedule. To
|
|
// guard against subtle reentrancy bugs, this microtask is the only place
|
|
// we do this — you can add roots to the schedule whenever, but you can
|
|
// only remove them here.
|
|
|
|
// Null this out so we know it's been removed from the schedule.
|
|
root.next = null;
|
|
if (prev === null) {
|
|
// This is the new head of the list
|
|
firstScheduledRoot = next;
|
|
} else {
|
|
prev.next = next;
|
|
}
|
|
if (next === null) {
|
|
// This is the new tail of the list
|
|
lastScheduledRoot = prev;
|
|
}
|
|
} else {
|
|
// This root still has work. Keep it in the list.
|
|
prev = root;
|
|
if (includesSyncLane(nextLanes)) {
|
|
mightHavePendingSyncWork = true;
|
|
}
|
|
}
|
|
root = next;
|
|
}
|
|
|
|
currentEventTransitionLane = NoLane;
|
|
|
|
// At the end of the microtask, flush any pending synchronous work. This has
|
|
// to come at the end, because it does actual rendering work that might throw.
|
|
flushSyncWorkOnAllRoots();
|
|
}
|
|
|
|
function scheduleTaskForRootDuringMicrotask(
|
|
root: FiberRoot,
|
|
currentTime: number,
|
|
): Lane {
|
|
// This function is always called inside a microtask, or at the very end of a
|
|
// rendering task right before we yield to the main thread. It should never be
|
|
// called synchronously.
|
|
//
|
|
// TODO: Unless enableDeferRootSchedulingToMicrotask is off. We need to land
|
|
// that ASAP to unblock additional features we have planned.
|
|
//
|
|
// This function also never performs React work synchronously; it should
|
|
// only schedule work to be performed later, in a separate task or microtask.
|
|
|
|
// Check if any lanes are being starved by other work. If so, mark them as
|
|
// expired so we know to work on those next.
|
|
markStarvedLanesAsExpired(root, currentTime);
|
|
|
|
// Determine the next lanes to work on, and their priority.
|
|
const workInProgressRoot = getWorkInProgressRoot();
|
|
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
|
|
const nextLanes = getNextLanes(
|
|
root,
|
|
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
|
|
);
|
|
|
|
const existingCallbackNode = root.callbackNode;
|
|
if (
|
|
// Check if there's nothing to work on
|
|
nextLanes === NoLanes ||
|
|
// If this root is currently suspended and waiting for data to resolve, don't
|
|
// schedule a task to render it. We'll either wait for a ping, or wait to
|
|
// receive an update.
|
|
//
|
|
// Suspended render phase
|
|
(root === workInProgressRoot && isWorkLoopSuspendedOnData()) ||
|
|
// Suspended commit phase
|
|
root.cancelPendingCommit !== null
|
|
) {
|
|
// Fast path: There's nothing to work on.
|
|
if (existingCallbackNode !== null) {
|
|
cancelCallback(existingCallbackNode);
|
|
}
|
|
root.callbackNode = null;
|
|
root.callbackPriority = NoLane;
|
|
return NoLane;
|
|
}
|
|
|
|
// Schedule a new callback in the host environment.
|
|
if (includesSyncLane(nextLanes)) {
|
|
// Synchronous work is always flushed at the end of the microtask, so we
|
|
// don't need to schedule an additional task.
|
|
if (existingCallbackNode !== null) {
|
|
cancelCallback(existingCallbackNode);
|
|
}
|
|
root.callbackPriority = SyncLane;
|
|
root.callbackNode = null;
|
|
return SyncLane;
|
|
} else {
|
|
// We use the highest priority lane to represent the priority of the callback.
|
|
const existingCallbackPriority = root.callbackPriority;
|
|
const newCallbackPriority = getHighestPriorityLane(nextLanes);
|
|
|
|
if (
|
|
newCallbackPriority === existingCallbackPriority &&
|
|
// Special case related to `act`. If the currently scheduled task is a
|
|
// Scheduler task, rather than an `act` task, cancel it and re-schedule
|
|
// on the `act` queue.
|
|
!(
|
|
__DEV__ &&
|
|
ReactSharedInternals.actQueue !== null &&
|
|
existingCallbackNode !== fakeActCallbackNode
|
|
)
|
|
) {
|
|
// The priority hasn't changed. We can reuse the existing task.
|
|
return newCallbackPriority;
|
|
} else {
|
|
// Cancel the existing callback. We'll schedule a new one below.
|
|
cancelCallback(existingCallbackNode);
|
|
}
|
|
|
|
let schedulerPriorityLevel;
|
|
switch (lanesToEventPriority(nextLanes)) {
|
|
case DiscreteEventPriority:
|
|
schedulerPriorityLevel = ImmediateSchedulerPriority;
|
|
break;
|
|
case ContinuousEventPriority:
|
|
schedulerPriorityLevel = UserBlockingSchedulerPriority;
|
|
break;
|
|
case DefaultEventPriority:
|
|
schedulerPriorityLevel = NormalSchedulerPriority;
|
|
break;
|
|
case IdleEventPriority:
|
|
schedulerPriorityLevel = IdleSchedulerPriority;
|
|
break;
|
|
default:
|
|
schedulerPriorityLevel = NormalSchedulerPriority;
|
|
break;
|
|
}
|
|
|
|
const newCallbackNode = scheduleCallback(
|
|
schedulerPriorityLevel,
|
|
performConcurrentWorkOnRoot.bind(null, root),
|
|
);
|
|
|
|
root.callbackPriority = newCallbackPriority;
|
|
root.callbackNode = newCallbackNode;
|
|
return newCallbackPriority;
|
|
}
|
|
}
|
|
|
|
export type RenderTaskFn = (didTimeout: boolean) => RenderTaskFn | null;
|
|
|
|
export function getContinuationForRoot(
|
|
root: FiberRoot,
|
|
originalCallbackNode: mixed,
|
|
): RenderTaskFn | null {
|
|
// This is called at the end of `performConcurrentWorkOnRoot` to determine
|
|
// if we need to schedule a continuation task.
|
|
//
|
|
// Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask;
|
|
// however, since most of the logic for determining if we need a continuation
|
|
// versus a new task is the same, we cheat a bit and call it here. This is
|
|
// only safe to do because we know we're at the end of the browser task.
|
|
// So although it's not an actual microtask, it might as well be.
|
|
scheduleTaskForRootDuringMicrotask(root, now());
|
|
if (root.callbackNode === originalCallbackNode) {
|
|
// The task node scheduled for this root is the same one that's
|
|
// currently executed. Need to return a continuation.
|
|
return performConcurrentWorkOnRoot.bind(null, root);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const fakeActCallbackNode = {};
|
|
|
|
function scheduleCallback(
|
|
priorityLevel: PriorityLevel,
|
|
callback: RenderTaskFn,
|
|
) {
|
|
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
|
|
// Special case: We're inside an `act` scope (a testing utility).
|
|
// Instead of scheduling work in the host environment, add it to a
|
|
// fake internal queue that's managed by the `act` implementation.
|
|
ReactSharedInternals.actQueue.push(callback);
|
|
return fakeActCallbackNode;
|
|
} else {
|
|
return Scheduler_scheduleCallback(priorityLevel, callback);
|
|
}
|
|
}
|
|
|
|
function cancelCallback(callbackNode: mixed) {
|
|
if (__DEV__ && callbackNode === fakeActCallbackNode) {
|
|
// Special `act` case: check if this is the fake callback node used by
|
|
// the `act` implementation.
|
|
} else if (callbackNode !== null) {
|
|
Scheduler_cancelCallback(callbackNode);
|
|
}
|
|
}
|
|
|
|
function scheduleImmediateTask(cb: () => mixed) {
|
|
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
|
|
// Special case: Inside an `act` scope, we push microtasks to the fake `act`
|
|
// callback queue. This is because we currently support calling `act`
|
|
// without awaiting the result. The plan is to deprecate that, and require
|
|
// that you always await the result so that the microtasks have a chance to
|
|
// run. But it hasn't happened yet.
|
|
ReactSharedInternals.actQueue.push(() => {
|
|
cb();
|
|
return null;
|
|
});
|
|
}
|
|
|
|
// TODO: Can we land supportsMicrotasks? Which environments don't support it?
|
|
// Alternatively, can we move this check to the host config?
|
|
if (supportsMicrotasks) {
|
|
scheduleMicrotask(() => {
|
|
// In Safari, appending an iframe forces microtasks to run.
|
|
// https://github.com/facebook/react/issues/22459
|
|
// We don't support running callbacks in the middle of render
|
|
// or commit so we need to check against that.
|
|
const executionContext = getExecutionContext();
|
|
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
|
|
// Note that this would still prematurely flush the callbacks
|
|
// if this happens outside render or commit phase (e.g. in an event).
|
|
|
|
// Intentionally using a macrotask instead of a microtask here. This is
|
|
// wrong semantically but it prevents an infinite loop. The bug is
|
|
// Safari's, not ours, so we just do our best to not crash even though
|
|
// the behavior isn't completely correct.
|
|
Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
|
|
return;
|
|
}
|
|
cb();
|
|
});
|
|
} else {
|
|
// If microtasks are not supported, use Scheduler.
|
|
Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb);
|
|
}
|
|
}
|
|
|
|
export function requestTransitionLane(
|
|
// This argument isn't used, it's only here to encourage the caller to
|
|
// check that it's inside a transition before calling this function.
|
|
// TODO: Make this non-nullable. Requires a tweak to useOptimistic.
|
|
transition: BatchConfigTransition | null,
|
|
): Lane {
|
|
// The algorithm for assigning an update to a lane should be stable for all
|
|
// updates at the same priority within the same event. To do this, the
|
|
// inputs to the algorithm must be the same.
|
|
//
|
|
// The trick we use is to cache the first of each of these inputs within an
|
|
// event. Then reset the cached values once we can be sure the event is
|
|
// over. Our heuristic for that is whenever we enter a concurrent work loop.
|
|
if (currentEventTransitionLane === NoLane) {
|
|
// All transitions within the same event are assigned the same lane.
|
|
currentEventTransitionLane = claimNextTransitionLane();
|
|
}
|
|
return currentEventTransitionLane;
|
|
}
|
|
|
|
export function didCurrentEventScheduleTransition(): boolean {
|
|
return currentEventTransitionLane !== NoLane;
|
|
}
|