Skip to content

Commit 086ab1a

Browse files
committed
Pick up the FormReplayingQueue from the root when a new boundary hydrates
Then try to replay any action that got unblocked. The hard part comes from the it might be more than one React instance that shares this queue.
1 parent bd3646e commit 086ab1a

File tree

3 files changed

+154
-5
lines changed

3 files changed

+154
-5
lines changed

packages/react-dom-bindings/src/events/ReactDOMEventListener.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,19 +225,25 @@ export function dispatchEvent(
225225
);
226226
}
227227

228+
export function findInstanceBlockingEvent(
229+
nativeEvent: AnyNativeEvent,
230+
): null | Container | SuspenseInstance {
231+
const nativeEventTarget = getEventTarget(nativeEvent);
232+
return findInstanceBlockingTarget(nativeEventTarget);
233+
}
234+
228235
export let return_targetInst: null | Fiber = null;
229236

230237
// Returns a SuspenseInstance or Container if it's blocked.
231238
// The return_targetInst field above is conceptually part of the return value.
232-
export function findInstanceBlockingEvent(
233-
nativeEvent: AnyNativeEvent,
239+
export function findInstanceBlockingTarget(
240+
targetNode: Node,
234241
): null | Container | SuspenseInstance {
235242
// TODO: Warn if _enabled is false.
236243

237244
return_targetInst = null;
238245

239-
const nativeEventTarget = getEventTarget(nativeEvent);
240-
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
246+
let targetInst = getClosestInstanceFromNode(targetNode);
241247

242248
if (targetInst !== null) {
243249
const nearestMounted = getNearestMountedFiber(targetInst);

packages/react-dom-bindings/src/events/ReactDOMEventReplaying.js

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,20 @@ import {
2323
getContainerFromFiber,
2424
getSuspenseInstanceFromFiber,
2525
} from 'react-reconciler/src/ReactFiberTreeReflection';
26-
import {findInstanceBlockingEvent} from './ReactDOMEventListener';
26+
import {
27+
findInstanceBlockingEvent,
28+
findInstanceBlockingTarget,
29+
} from './ReactDOMEventListener';
2730
import {setReplayingEvent, resetReplayingEvent} from './CurrentReplayingEvent';
2831
import {
2932
getInstanceFromNode,
3033
getClosestInstanceFromNode,
34+
getFiberCurrentPropsFromNode,
3135
} from '../client/ReactDOMComponentTree';
3236
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
3337
import {isHigherEventPriority} from 'react-reconciler/src/ReactEventPriorities';
3438
import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration';
39+
import {dispatchReplayedFormAction} from './plugins/FormActionEventPlugin';
3540

3641
import {
3742
attemptContinuousHydration,
@@ -41,6 +46,7 @@ import {
4146
runWithPriority as attemptHydrationAtPriority,
4247
getCurrentUpdatePriority,
4348
} from 'react-reconciler/src/ReactEventPriorities';
49+
import {enableFormActions} from 'shared/ReactFeatureFlags';
4450

4551
// TODO: Upgrade this definition once we're on a newer version of Flow that
4652
// has this definition built-in.
@@ -430,6 +436,67 @@ function scheduleCallbackIfUnblocked(
430436
}
431437
}
432438

439+
type FormAction = FormData => void | Promise<void>;
440+
441+
type FormReplayingQueue = Array<any>; // [form, submitter or action, formData...]
442+
443+
let lastScheduledReplayQueue: null | FormReplayingQueue = null;
444+
445+
function replayUnblockedFormActions(formReplayingQueue: FormReplayingQueue) {
446+
if (lastScheduledReplayQueue === formReplayingQueue) {
447+
lastScheduledReplayQueue = null;
448+
}
449+
for (let i = 0; i < formReplayingQueue.length; i += 3) {
450+
const form: HTMLFormElement = formReplayingQueue[i];
451+
const submitterOrAction:
452+
| null
453+
| HTMLInputElement
454+
| HTMLButtonElement
455+
| FormAction = formReplayingQueue[i + 1];
456+
const formData: FormData = formReplayingQueue[i + 2];
457+
if (typeof submitterOrAction !== 'function') {
458+
// This action is not hydrated yet. This might be because it's blocked on
459+
// a different React instance or higher up our tree.
460+
const blockedOn = findInstanceBlockingTarget(submitterOrAction || form);
461+
if (blockedOn === null) {
462+
// We're not blocked but we don't have an action. This must mean that
463+
// this is in another React instance. We'll just skip past it.
464+
continue;
465+
} else {
466+
// We're blocked on something in this React instance. We'll retry later.
467+
break;
468+
}
469+
}
470+
const formInst = getInstanceFromNode(form);
471+
if (formInst !== null) {
472+
// This is part of our instance.
473+
// We're ready to replay this. Let's delete it from the queue.
474+
formReplayingQueue.splice(i, 3);
475+
i -= 3;
476+
dispatchReplayedFormAction(formInst, submitterOrAction, formData);
477+
// Continue without incrementing the index.
478+
continue;
479+
}
480+
// This form must've been part of a different React instance.
481+
// If we want to preserve ordering between React instances on the same root
482+
// we'd need some way for the other instance to ping us when it's done.
483+
// We'll just skip this and let the other instance execute it.
484+
}
485+
}
486+
487+
function scheduleReplayQueueIfNeeded(formReplayingQueue: FormReplayingQueue) {
488+
// Schedule a callback to execute any unblocked form actions in.
489+
// We only keep track of the last queue which means that if multiple React oscillate
490+
// commits, we could schedule more callbacks than necessary but it's not a big deal
491+
// and we only really except one instance.
492+
if (lastScheduledReplayQueue !== formReplayingQueue) {
493+
lastScheduledReplayQueue = formReplayingQueue;
494+
scheduleCallback(NormalPriority, () =>
495+
replayUnblockedFormActions(formReplayingQueue),
496+
);
497+
}
498+
}
499+
433500
export function retryIfBlockedOn(
434501
unblocked: Container | SuspenseInstance,
435502
): void {
@@ -467,4 +534,72 @@ export function retryIfBlockedOn(
467534
}
468535
}
469536
}
537+
538+
if (enableFormActions) {
539+
// Check the document if there are any queued form actions.
540+
const root = unblocked.getRootNode();
541+
const formReplayingQueue: void | FormReplayingQueue = (root: any)
542+
.$$reactFormReplay;
543+
if (formReplayingQueue != null) {
544+
for (let i = 0; i < formReplayingQueue.length; i += 3) {
545+
const form: HTMLFormElement = formReplayingQueue[i];
546+
const submitterOrAction:
547+
| null
548+
| HTMLInputElement
549+
| HTMLButtonElement
550+
| FormAction = formReplayingQueue[i + 1];
551+
const formProps = getFiberCurrentPropsFromNode(form);
552+
if (typeof submitterOrAction === 'function') {
553+
// This action has already resolved. We're just waiting to dispatch it.
554+
if (!formProps) {
555+
// This was not part of this React instance. It might have been recently
556+
// unblocking us from dispatching our events. So let's make sure we schedule
557+
// a retry.
558+
scheduleReplayQueueIfNeeded(formReplayingQueue);
559+
}
560+
continue;
561+
}
562+
let target: Node = form;
563+
if (formProps) {
564+
// This form belongs to this React instance but the submitter might
565+
// not be done yet.
566+
let action: null | FormAction = null;
567+
const submitter = submitterOrAction;
568+
if (submitter && submitter.hasAttribute('formAction')) {
569+
// The submitter is the one that is responsible for the action.
570+
target = submitter;
571+
const submitterProps = getFiberCurrentPropsFromNode(submitter);
572+
if (submitterProps) {
573+
// The submitter is part of this instance.
574+
action = (submitterProps: any).formAction;
575+
} else {
576+
const blockedOn = findInstanceBlockingTarget(target);
577+
if (blockedOn !== null) {
578+
// The submitter is not hydrated yet. We'll wait for it.
579+
continue;
580+
}
581+
// The submitter must have been a part of a different React instance.
582+
// Except the form isn't. We don't dispatch actions in this scenario.
583+
}
584+
} else {
585+
action = (formProps: any).action;
586+
}
587+
if (typeof action === 'function') {
588+
formReplayingQueue[i + 1] = action;
589+
} else {
590+
// Something went wrong so let's just delete this action.
591+
formReplayingQueue.splice(i, 3);
592+
i -= 3;
593+
}
594+
// Schedule a replay in case this unblocked something.
595+
scheduleReplayQueueIfNeeded(formReplayingQueue);
596+
continue;
597+
}
598+
// Something above this target is still blocked so we can't continue yet.
599+
// We're not sure if this target is actually part of this React instance
600+
// yet. It could be a different React as a child but at least some parent is.
601+
// We must continue for any further queued actions.
602+
}
603+
}
604+
}
470605
}

packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,11 @@ function extractEvents(
116116
}
117117

118118
export {extractEvents};
119+
120+
export function dispatchReplayedFormAction(
121+
formInst: Fiber,
122+
action: FormData => void | Promise<void>,
123+
formData: FormData,
124+
): void {
125+
startHostTransition(formInst, action, formData);
126+
}

0 commit comments

Comments
 (0)