23
getContainerFromFiber,
24
getSuspenseInstanceFromFiber,
25
} from 'react-reconciler/src/ReactFiberTreeReflection';
26
-import {findInstanceBlockingEvent} from './ReactDOMEventListener';
26
+import {
27
+ findInstanceBlockingEvent,
28
+ findInstanceBlockingTarget,
29
+} from './ReactDOMEventListener';
30
import {setReplayingEvent, resetReplayingEvent} from './CurrentReplayingEvent';
31
import {
32
getInstanceFromNode,
33
getClosestInstanceFromNode,
34
+ getFiberCurrentPropsFromNode,
35
} from '../client/ReactDOMComponentTree';
36
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
37
import {isHigherEventPriority} from 'react-reconciler/src/ReactEventPriorities';
38
import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration';
39
+import {dispatchReplayedFormAction} from './plugins/FormActionEventPlugin';
40
41
import {
42
attemptContinuousHydration,
46
runWithPriority as attemptHydrationAtPriority,
47
getCurrentUpdatePriority,
48
} from 'react-reconciler/src/ReactEventPriorities';
49
+import {enableFormActions} from 'shared/ReactFeatureFlags';
50
51
// TODO: Upgrade this definition once we're on a newer version of Flow that
52
// has this definition built-in.
111
'change',
112
'contextmenu',
113
'reset',
108
- 'submit',
114
+ // 'submit', // stopPropagation blocks the replay mechanism
115
];
116
117
export function isDiscreteEventThatRequiresHydration(
436
}
437
}
438
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
+
500
export function retryIfBlockedOn(
501
unblocked: Container | SuspenseInstance,
502
): void {
534
}
535
}
536
}
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
+ }
605
}