@@ -23,15 +23,20 @@ import {
23
23
getContainerFromFiber ,
24
24
getSuspenseInstanceFromFiber ,
25
25
} from 'react-reconciler/src/ReactFiberTreeReflection' ;
26
- import { findInstanceBlockingEvent } from './ReactDOMEventListener' ;
26
+ import {
27
+ findInstanceBlockingEvent ,
28
+ findInstanceBlockingTarget ,
29
+ } from './ReactDOMEventListener' ;
27
30
import { setReplayingEvent , resetReplayingEvent } from './CurrentReplayingEvent' ;
28
31
import {
29
32
getInstanceFromNode ,
30
33
getClosestInstanceFromNode ,
34
+ getFiberCurrentPropsFromNode ,
31
35
} from '../client/ReactDOMComponentTree' ;
32
36
import { HostRoot , SuspenseComponent } from 'react-reconciler/src/ReactWorkTags' ;
33
37
import { isHigherEventPriority } from 'react-reconciler/src/ReactEventPriorities' ;
34
38
import { isRootDehydrated } from 'react-reconciler/src/ReactFiberShellHydration' ;
39
+ import { dispatchReplayedFormAction } from './plugins/FormActionEventPlugin' ;
35
40
36
41
import {
37
42
attemptContinuousHydration ,
@@ -41,6 +46,7 @@ import {
41
46
runWithPriority as attemptHydrationAtPriority ,
42
47
getCurrentUpdatePriority ,
43
48
} from 'react-reconciler/src/ReactEventPriorities' ;
49
+ import { enableFormActions } from 'shared/ReactFeatureFlags' ;
44
50
45
51
// TODO: Upgrade this definition once we're on a newer version of Flow that
46
52
// has this definition built-in.
@@ -430,6 +436,67 @@ function scheduleCallbackIfUnblocked(
430
436
}
431
437
}
432
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
+
433
500
export function retryIfBlockedOn (
434
501
unblocked : Container | SuspenseInstance ,
435
502
) : void {
@@ -467,4 +534,72 @@ export function retryIfBlockedOn(
467
534
}
468
535
}
469
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
+ }
470
605
}
0 commit comments