Revert "Clean up Selective Hydration / Event Replay flag (#24156)" (#24402)
This reverts commit b5cca182ffd5500b83f20f215d0e16d6dbae0efb.
Ricky committed
Apr 19, 2022 at 17:34 UTC
a6d53f3468636bcee30c26d4e0df7a4582526d63
19 files changed
+565
-72
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+80
-11
@@ -2415,8 +2415,18 @@ describe('ReactDOMServerPartialHydration', () => {
2415
await promise;
2416
});
2417
2418
- expect(clicks).toBe(0);
2419
- expect(container.textContent).toBe('Click meHello');
2418
+ if (
2419
+ gate(
2420
+ flags =>
2421
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2422
+ )
2423
+ ) {
2424
+ expect(clicks).toBe(0);
2425
+ expect(container.textContent).toBe('Click meHello');
2426
+ } else {
2427
+ expect(clicks).toBe(1);
2428
+ expect(container.textContent).toBe('Hello');
2429
+ }
2430
document.body.removeChild(container);
2431
});
2432
@@ -2498,7 +2508,17 @@ describe('ReactDOMServerPartialHydration', () => {
2508
await promise;
2509
});
2510
2501
- expect(onEvent).toHaveBeenCalledTimes(0);
2511
+ if (
2512
+ gate(
2513
+ flags =>
2514
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2515
+ )
2516
+ ) {
2517
+ expect(onEvent).toHaveBeenCalledTimes(0);
2518
+ } else {
2519
+ expect(onEvent).toHaveBeenCalledTimes(2);
2520
+ }
2521
+
2522
document.body.removeChild(container);
2523
});
2524
@@ -2578,7 +2598,16 @@ describe('ReactDOMServerPartialHydration', () => {
2598
await promise;
2599
});
2600
2581
- expect(clicks).toBe(0);
2601
+ if (
2602
+ gate(
2603
+ flags =>
2604
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2605
+ )
2606
+ ) {
2607
+ expect(clicks).toBe(0);
2608
+ } else {
2609
+ expect(clicks).toBe(2);
2610
+ }
2611
2612
document.body.removeChild(container);
2613
});
@@ -2663,7 +2692,17 @@ describe('ReactDOMServerPartialHydration', () => {
2692
resolve();
2693
await promise;
2694
});
2666
- expect(onEvent).toHaveBeenCalledTimes(0);
2695
+ if (
2696
+ gate(
2697
+ flags =>
2698
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2699
+ )
2700
+ ) {
2701
+ expect(onEvent).toHaveBeenCalledTimes(0);
2702
+ } else {
2703
+ expect(onEvent).toHaveBeenCalledTimes(2);
2704
+ }
2705
+
2706
document.body.removeChild(container);
2707
});
2708
@@ -2734,8 +2773,19 @@ describe('ReactDOMServerPartialHydration', () => {
2773
await promise;
2774
});
2775
2737
- expect(clicksOnChild).toBe(0);
2738
- expect(clicksOnParent).toBe(0);
2776
+ if (
2777
+ gate(
2778
+ flags =>
2779
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2780
+ )
2781
+ ) {
2782
+ expect(clicksOnChild).toBe(0);
2783
+ expect(clicksOnParent).toBe(0);
2784
+ } else {
2785
+ expect(clicksOnChild).toBe(1);
2786
+ // This will be zero due to the stopPropagation.
2787
+ expect(clicksOnParent).toBe(0);
2788
+ }
2789
2790
document.body.removeChild(container);
2791
});
@@ -2811,7 +2861,16 @@ describe('ReactDOMServerPartialHydration', () => {
2861
});
2862
2863
// We're now full hydrated.
2814
- expect(clicks).toBe(0);
2864
+ if (
2865
+ gate(
2866
+ flags =>
2867
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2868
+ )
2869
+ ) {
2870
+ expect(clicks).toBe(0);
2871
+ } else {
2872
+ expect(clicks).toBe(1);
2873
+ }
2874
2875
document.body.removeChild(parentContainer);
2876
});
@@ -3080,9 +3139,19 @@ describe('ReactDOMServerPartialHydration', () => {
3139
await promise;
3140
});
3141
3083
- // discrete event not replayed
3084
- expect(submits).toBe(0);
3085
- expect(container.textContent).toBe('Click meHello');
3142
+ if (
3143
+ gate(
3144
+ flags =>
3145
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
3146
+ )
3147
+ ) {
3148
+ // discrete event not replayed
3149
+ expect(submits).toBe(0);
3150
+ expect(container.textContent).toBe('Click meHello');
3151
+ } else {
3152
+ expect(submits).toBe(1);
3153
+ expect(container.textContent).toBe('Hello');
3154
+ }
3155
3156
document.body.removeChild(container);
3157
});
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+176
-46
@@ -268,7 +268,18 @@ describe('ReactDOMServerSelectiveHydration', () => {
268
await promise;
269
});
270
271
- expect(Scheduler).toHaveYielded(['D', 'A']);
271
+ if (
272
+ gate(
273
+ flags =>
274
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
275
+ )
276
+ ) {
277
+ expect(Scheduler).toHaveYielded(['D', 'A']);
278
+ } else {
279
+ // After the click, we should prioritize D and the Click first,
280
+ // and only after that render A and C.
281
+ expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']);
282
+ }
283
284
document.body.removeChild(container);
285
});
@@ -342,7 +353,16 @@ describe('ReactDOMServerSelectiveHydration', () => {
353
dispatchClickEvent(spanC);
354
dispatchClickEvent(spanD);
355
345
- expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']);
356
+ if (
357
+ gate(
358
+ flags =>
359
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
360
+ )
361
+ ) {
362
+ expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']);
363
+ } else {
364
+ expect(Scheduler).toHaveYielded(['App']);
365
+ }
366
367
await act(async () => {
368
suspend = false;
@@ -350,12 +370,29 @@ describe('ReactDOMServerSelectiveHydration', () => {
370
await promise;
371
});
372
353
- expect(Scheduler).toHaveYielded([
354
- 'A',
355
- 'D',
356
- // B should render last since it wasn't clicked.
357
- 'B',
358
- ]);
373
+ if (
374
+ ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
375
+ ) {
376
+ expect(Scheduler).toHaveYielded([
377
+ 'A',
378
+ 'D',
379
+ // B should render last since it wasn't clicked.
380
+ 'B',
381
+ ]);
382
+ } else {
383
+ // We should prioritize hydrating A, C and D first since we clicked in
384
+ // them. Only after they're done will we hydrate B.
385
+ expect(Scheduler).toHaveYielded([
386
+ 'A',
387
+ 'Clicked A',
388
+ 'C',
389
+ 'Clicked C',
390
+ 'D',
391
+ 'Clicked D',
392
+ // B should render last since it wasn't clicked.
393
+ 'B',
394
+ ]);
395
+ }
396
397
document.body.removeChild(container);
398
});
@@ -509,8 +546,17 @@ describe('ReactDOMServerSelectiveHydration', () => {
546
resolve();
547
await promise;
548
});
512
- // no replay
513
- expect(Scheduler).toHaveYielded(['D', 'A']);
549
+ if (
550
+ gate(
551
+ flags =>
552
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
553
+ )
554
+ ) {
555
+ // no replay
556
+ expect(Scheduler).toHaveYielded(['D', 'A']);
557
+ } else {
558
+ expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']);
559
+ }
560
561
document.body.removeChild(container);
562
});
@@ -589,19 +635,42 @@ describe('ReactDOMServerSelectiveHydration', () => {
635
createEventTarget(spanC).virtualclick();
636
createEventTarget(spanD).virtualclick();
637
592
- expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']);
638
+ if (
639
+ ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
640
+ ) {
641
+ expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']);
642
+ } else {
643
+ expect(Scheduler).toHaveYielded(['App']);
644
+ }
645
await act(async () => {
646
suspend = false;
647
resolve();
648
await promise;
649
});
650
599
- expect(Scheduler).toHaveYielded([
600
- 'A',
601
- 'D',
602
- // B should render last since it wasn't clicked.
603
- 'B',
604
- ]);
651
+ if (
652
+ ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
653
+ ) {
654
+ expect(Scheduler).toHaveYielded([
655
+ 'A',
656
+ 'D',
657
+ // B should render last since it wasn't clicked.
658
+ 'B',
659
+ ]);
660
+ } else {
661
+ // We should prioritize hydrating A, C and D first since we clicked in
662
+ // them. Only after they're done will we hydrate B.
663
+ expect(Scheduler).toHaveYielded([
664
+ 'A',
665
+ 'Clicked A',
666
+ 'C',
667
+ 'Clicked C',
668
+ 'D',
669
+ 'Clicked D',
670
+ // B should render last since it wasn't clicked.
671
+ 'B',
672
+ ]);
673
+ }
674
675
document.body.removeChild(container);
676
});
@@ -681,15 +750,37 @@ describe('ReactDOMServerSelectiveHydration', () => {
750
resolve();
751
await promise;
752
});
684
- // We should prioritize hydrating D first because we clicked it.
685
- // but event isnt replayed
686
- expect(Scheduler).toHaveYielded([
687
- 'D',
688
- 'B', // Ideally this should be later.
689
- 'C',
690
- 'Hover C',
691
- 'A',
692
- ]);
753
+ if (
754
+ gate(
755
+ flags =>
756
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
757
+ )
758
+ ) {
759
+ // We should prioritize hydrating D first because we clicked it.
760
+ // but event isnt replayed
761
+ expect(Scheduler).toHaveYielded([
762
+ 'D',
763
+ 'B', // Ideally this should be later.
764
+ 'C',
765
+ 'Hover C',
766
+ 'A',
767
+ ]);
768
+ } else {
769
+ // We should prioritize hydrating D first because we clicked it.
770
+ // Next we should hydrate C since that's the current hover target.
771
+ // To simplify implementation details we hydrate both B and C at
772
+ // the same time since B was already scheduled.
773
+ // This is ok because it will at least not continue for nested
774
+ // boundary. See the next test below.
775
+ expect(Scheduler).toHaveYielded([
776
+ 'D',
777
+ 'Clicked D',
778
+ 'B', // Ideally this should be later.
779
+ 'C',
780
+ 'Hover C',
781
+ 'A',
782
+ ]);
783
+ }
784
785
document.body.removeChild(container);
786
});
@@ -813,22 +904,47 @@ describe('ReactDOMServerSelectiveHydration', () => {
904
await promise;
905
});
906
816
- // We should prioritize hydrating D first because we clicked it.
817
- // but event isnt replayed
818
- expect(Scheduler).toHaveYielded([
819
- 'D',
820
- 'B', // Ideally this should be later.
821
- 'C',
822
- // Mouse out events aren't replayed
823
- // 'Mouse Out Capture B',
824
- // 'Mouse Out B',
825
- 'Mouse Over Capture Parent',
826
- 'Mouse Over Capture C',
827
- // Stop propagation stops these
828
- // 'Mouse Over Capture Inner C',
829
- // 'Mouse Over C',
830
- 'A',
831
- ]);
907
+ if (
908
+ gate(
909
+ flags =>
910
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
911
+ )
912
+ ) {
913
+ // We should prioritize hydrating D first because we clicked it.
914
+ // but event isnt replayed
915
+ expect(Scheduler).toHaveYielded([
916
+ 'D',
917
+ 'B', // Ideally this should be later.
918
+ 'C',
919
+ // Mouse out events aren't replayed
920
+ // 'Mouse Out Capture B',
921
+ // 'Mouse Out B',
922
+ 'Mouse Over Capture Parent',
923
+ 'Mouse Over Capture C',
924
+ // Stop propagation stops these
925
+ // 'Mouse Over Capture Inner C',
926
+ // 'Mouse Over C',
927
+ 'A',
928
+ ]);
929
+ } else {
930
+ // We should prioritize hydrating D first because we clicked it.
931
+ // Next we should hydrate C since that's the current hover target.
932
+ // To simplify implementation details we hydrate both B and C at
933
+ // the same time since B was already scheduled.
934
+ // This is ok because it will at least not continue for nested
935
+ // boundary. See the next test below.
936
+ expect(Scheduler).toHaveYielded([
937
+ 'D',
938
+ 'Clicked D',
939
+ 'B', // Ideally this should be later.
940
+ 'C',
941
+ // Capture phase isn't replayed
942
+ // Mouseout isn't replayed
943
+ 'Mouse Over C',
944
+ 'Mouse Enter C',
945
+ 'A',
946
+ ]);
947
+ }
948
949
// This test shows existing quirk where stopPropagation on mouseout
950
// prevents mouseEnter from firing
@@ -975,10 +1091,19 @@ describe('ReactDOMServerSelectiveHydration', () => {
1091
});
1092
1093
expect(OuterScheduler).toHaveYielded(['Suspend Outer']);
978
-
979
- // InnerApp doesn't see the event because OuterApp calls stopPropagation in
980
- // capture phase since the event is blocked on suspended component
981
- expect(InnerScheduler).toHaveYielded([]);
1094
+ if (
1095
+ gate(
1096
+ flags =>
1097
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
1098
+ )
1099
+ ) {
1100
+ // InnerApp doesn't see the event because OuterApp calls stopPropagation in
1101
+ // capture phase since the event is blocked on suspended component
1102
+ expect(InnerScheduler).toHaveYielded([]);
1103
+ } else {
1104
+ // no stopPropagation
1105
+ expect(InnerScheduler).toHaveYielded(['Suspend Inner']);
1106
+ }
1107
1108
expect(Scheduler).toHaveYielded([]);
1109
});
@@ -986,6 +1111,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
1111
document.body.innerHTML = '';
1112
});
1113
1114
+ // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1115
it('Inner hydrates first then Outer', async () => {
1116
dispatchMouseHoverEvent(innerDiv);
1117
@@ -1042,6 +1168,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
1168
]);
1169
});
1170
1171
+ // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1172
it('Outer hydrates first then Inner', async () => {
1173
dispatchMouseHoverEvent(innerDiv);
1174
@@ -1103,6 +1230,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
1230
});
1231
});
1232
1233
+ // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1234
it('replays event with null target when tree is dismounted', async () => {
1235
let suspend = false;
1236
let resolve;
@@ -1421,6 +1549,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
1549
document.body.removeChild(container);
1550
});
1551
1552
+ // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1553
it('fires capture event handlers and native events if content is hydratable during discrete event', async () => {
1554
spyOnDev(console, 'error');
1555
function Child({text}) {
@@ -1504,6 +1633,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
1633
document.body.removeChild(container);
1634
});
1635
1636
+ // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1637
it('does not propagate discrete event if it cannot be synchronously hydrated', async () => {
1638
let triggeredParent = false;
1639
let triggeredChild = false;
packages/react-dom/src/client/ReactDOM.js
+3
@@ -37,6 +37,7 @@ import {
37
flushControlled,
38
injectIntoDevTools,
39
attemptSynchronousHydration,
40
+ attemptDiscreteHydration,
41
attemptContinuousHydration,
42
attemptHydrationAtCurrentPriority,
43
} from 'react-reconciler/src/ReactFiberReconciler';
@@ -58,6 +59,7 @@ import {
59
import {restoreControlledState} from './ReactDOMComponent';
60
import {
61
setAttemptSynchronousHydration,
62
+ setAttemptDiscreteHydration,
63
setAttemptContinuousHydration,
64
setAttemptHydrationAtCurrentPriority,
65
setGetCurrentUpdatePriority,
@@ -71,6 +73,7 @@ import {
73
} from '../events/ReactDOMControlledComponent';
74
75
setAttemptSynchronousHydration(attemptSynchronousHydration);
76
+setAttemptDiscreteHydration(attemptDiscreteHydration);
77
setAttemptContinuousHydration(attemptContinuousHydration);
78
setAttemptHydrationAtCurrentPriority(attemptHydrationAtCurrentPriority);
79
setGetCurrentUpdatePriority(getCurrentUpdatePriority);
packages/react-dom/src/events/ReactDOMEventListener.js
+121
-2
@@ -11,8 +11,11 @@ import type {AnyNativeEvent} from '../events/PluginModuleType';
11
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
12
import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
13
import type {DOMEventName} from '../events/DOMEventNames';
14
+import {enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay} from 'shared/ReactFeatureFlags';
15
import {
16
isDiscreteEventThatRequiresHydration,
17
+ queueDiscreteEvent,
18
+ hasQueuedDiscreteEvents,
19
clearIfContinuousEvent,
20
queueIfContinuousEvent,
21
attemptSynchronousHydration,
@@ -143,7 +146,123 @@ function dispatchContinuousEvent(
146
}
147
}
148
146
-function dispatchEvent(
149
+export function dispatchEvent(
150
+ domEventName: DOMEventName,
151
+ eventSystemFlags: EventSystemFlags,
152
+ targetContainer: EventTarget,
153
+ nativeEvent: AnyNativeEvent,
154
+): void {
155
+ if (!_enabled) {
156
+ return;
157
+ }
158
+ if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
159
+ dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
160
+ domEventName,
161
+ eventSystemFlags,
162
+ targetContainer,
163
+ nativeEvent,
164
+ );
165
+ } else {
166
+ dispatchEventOriginal(
167
+ domEventName,
168
+ eventSystemFlags,
169
+ targetContainer,
170
+ nativeEvent,
171
+ );
172
+ }
173
+}
174
+
175
+function dispatchEventOriginal(
176
+ domEventName: DOMEventName,
177
+ eventSystemFlags: EventSystemFlags,
178
+ targetContainer: EventTarget,
179
+ nativeEvent: AnyNativeEvent,
180
+) {
181
+ // TODO: replaying capture phase events is currently broken
182
+ // because we used to do it during top-level native bubble handlers
183
+ // but now we use different bubble and capture handlers.
184
+ // In eager mode, we attach capture listeners early, so we need
185
+ // to filter them out until we fix the logic to handle them correctly.
186
+ const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
187
+
188
+ if (
189
+ allowReplay &&
190
+ hasQueuedDiscreteEvents() &&
191
+ isDiscreteEventThatRequiresHydration(domEventName)
192
+ ) {
193
+ // If we already have a queue of discrete events, and this is another discrete
194
+ // event, then we can't dispatch it regardless of its target, since they
195
+ // need to dispatch in order.
196
+ queueDiscreteEvent(
197
+ null, // Flags that we're not actually blocked on anything as far as we know.
198
+ domEventName,
199
+ eventSystemFlags,
200
+ targetContainer,
201
+ nativeEvent,
202
+ );
203
+ return;
204
+ }
205
+
206
+ const blockedOn = findInstanceBlockingEvent(
207
+ domEventName,
208
+ eventSystemFlags,
209
+ targetContainer,
210
+ nativeEvent,
211
+ );
212
+ if (blockedOn === null) {
213
+ dispatchEventForPluginEventSystem(
214
+ domEventName,
215
+ eventSystemFlags,
216
+ nativeEvent,
217
+ return_targetInst,
218
+ targetContainer,
219
+ );
220
+ if (allowReplay) {
221
+ clearIfContinuousEvent(domEventName, nativeEvent);
222
+ }
223
+ return;
224
+ }
225
+
226
+ if (allowReplay) {
227
+ if (isDiscreteEventThatRequiresHydration(domEventName)) {
228
+ // This this to be replayed later once the target is available.
229
+ queueDiscreteEvent(
230
+ blockedOn,
231
+ domEventName,
232
+ eventSystemFlags,
233
+ targetContainer,
234
+ nativeEvent,
235
+ );
236
+ return;
237
+ }
238
+ if (
239
+ queueIfContinuousEvent(
240
+ blockedOn,
241
+ domEventName,
242
+ eventSystemFlags,
243
+ targetContainer,
244
+ nativeEvent,
245
+ )
246
+ ) {
247
+ return;
248
+ }
249
+ // We need to clear only if we didn't queue because
250
+ // queueing is accumulative.
251
+ clearIfContinuousEvent(domEventName, nativeEvent);
252
+ }
253
+
254
+ // This is not replayable so we'll invoke it but without a target,
255
+ // in case the event system needs to trace it.
256
+ dispatchEventForPluginEventSystem(
257
+ domEventName,
258
+ eventSystemFlags,
259
+ nativeEvent,
260
+ null,
261
+ targetContainer,
262
+ );
263
+}
264
+
265
+function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
266
domEventName: DOMEventName,
267
eventSystemFlags: EventSystemFlags,
268
targetContainer: EventTarget,
@@ -229,7 +348,7 @@ function dispatchEvent(
348
);
349
}
350
232
-let return_targetInst = null;
351
+export let return_targetInst = null;
352
353
// Returns a SuspenseInstance or Container if it's blocked.
354
// The return_targetInst field above is conceptually part of the return value.
packages/react-dom/src/events/ReactDOMEventReplaying.js
+130
-12
@@ -14,7 +14,10 @@ import type {EventSystemFlags} from './EventSystemFlags';
14
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
15
import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
16
17
-import {enableSelectiveHydration} from 'shared/ReactFeatureFlags';
17
+import {
18
+ enableSelectiveHydration,
19
+ enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
20
+} from 'shared/ReactFeatureFlags';
21
import {
22
unstable_scheduleCallback as scheduleCallback,
23
unstable_NormalPriority as NormalPriority,
@@ -24,8 +27,12 @@ import {
27
getContainerFromFiber,
28
getSuspenseInstanceFromFiber,
29
} from 'react-reconciler/src/ReactFiberTreeReflection';
27
-import {findInstanceBlockingEvent} from './ReactDOMEventListener';
30
+import {
31
+ findInstanceBlockingEvent,
32
+ return_targetInst,
33
+} from './ReactDOMEventListener';
34
import {setReplayingEvent, resetReplayingEvent} from './CurrentReplayingEvent';
35
+import {dispatchEventForPluginEventSystem} from './DOMPluginEventSystem';
36
import {
37
getInstanceFromNode,
38
getClosestInstanceFromNode,
@@ -44,6 +51,12 @@ export function attemptSynchronousHydration(fiber: Object) {
51
_attemptSynchronousHydration(fiber);
52
}
53
54
+let attemptDiscreteHydration: (fiber: Object) => void;
55
+
56
+export function setAttemptDiscreteHydration(fn: (fiber: Object) => void) {
57
+ attemptDiscreteHydration = fn;
58
+}
59
+
60
let attemptContinuousHydration: (fiber: Object) => void;
61
62
export function setAttemptContinuousHydration(fn: (fiber: Object) => void) {
@@ -120,7 +133,7 @@ export function hasQueuedContinuousEvents(): boolean {
133
return hasAnyQueuedContinuousEvents;
134
}
135
123
-const synchronouslyHydratedEvents: Array<DOMEventName> = [
136
+const discreteReplayableEvents: Array<DOMEventName> = [
137
'mousedown',
138
'mouseup',
139
'touchcancel',
@@ -154,7 +167,7 @@ const synchronouslyHydratedEvents: Array<DOMEventName> = [
167
export function isDiscreteEventThatRequiresHydration(
168
eventType: DOMEventName,
169
): boolean {
157
- return synchronouslyHydratedEvents.indexOf(eventType) > -1;
170
+ return discreteReplayableEvents.indexOf(eventType) > -1;
171
}
172
173
function createQueuedReplayableEvent(
@@ -173,6 +186,50 @@ function createQueuedReplayableEvent(
186
};
187
}
188
189
+export function queueDiscreteEvent(
190
+ blockedOn: null | Container | SuspenseInstance,
191
+ domEventName: DOMEventName,
192
+ eventSystemFlags: EventSystemFlags,
193
+ targetContainer: EventTarget,
194
+ nativeEvent: AnyNativeEvent,
195
+): void {
196
+ if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
197
+ return;
198
+ }
199
+ const queuedEvent = createQueuedReplayableEvent(
200
+ blockedOn,
201
+ domEventName,
202
+ eventSystemFlags,
203
+ targetContainer,
204
+ nativeEvent,
205
+ );
206
+ queuedDiscreteEvents.push(queuedEvent);
207
+ if (enableSelectiveHydration) {
208
+ if (queuedDiscreteEvents.length === 1) {
209
+ // If this was the first discrete event, we might be able to
210
+ // synchronously unblock it so that preventDefault still works.
211
+ while (queuedEvent.blockedOn !== null) {
212
+ const fiber = getInstanceFromNode(queuedEvent.blockedOn);
213
+ if (fiber === null) {
214
+ break;
215
+ }
216
+ attemptSynchronousHydration(fiber);
217
+ if (queuedEvent.blockedOn === null) {
218
+ // We got unblocked by hydration. Let's try again.
219
+ replayUnblockedEvents();
220
+ // If we're reblocked, on an inner boundary, we might need
221
+ // to attempt hydrating that one.
222
+ continue;
223
+ } else {
224
+ // We're still blocked from hydration, we have to give up
225
+ // and replay later.
226
+ break;
227
+ }
228
+ }
229
+ }
230
+ }
231
+}
232
+
233
// Resets the replaying for this type of continuous event to no event.
234
export function clearIfContinuousEvent(
235
domEventName: DOMEventName,
@@ -416,14 +473,26 @@ function attemptReplayContinuousQueuedEvent(
473
queuedEvent.nativeEvent,
474
);
475
if (nextBlockedOn === null) {
419
- const nativeEvent = queuedEvent.nativeEvent;
420
- const nativeEventClone = new nativeEvent.constructor(
421
- nativeEvent.type,
422
- (nativeEvent: any),
423
- );
424
- setReplayingEvent(nativeEventClone);
425
- nativeEvent.target.dispatchEvent(nativeEventClone);
426
- resetReplayingEvent();
476
+ if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
477
+ const nativeEvent = queuedEvent.nativeEvent;
478
+ const nativeEventClone = new nativeEvent.constructor(
479
+ nativeEvent.type,
480
+ (nativeEvent: any),
481
+ );
482
+ setReplayingEvent(nativeEventClone);
483
+ nativeEvent.target.dispatchEvent(nativeEventClone);
484
+ resetReplayingEvent();
485
+ } else {
486
+ setReplayingEvent(queuedEvent.nativeEvent);
487
+ dispatchEventForPluginEventSystem(
488
+ queuedEvent.domEventName,
489
+ queuedEvent.eventSystemFlags,
490
+ queuedEvent.nativeEvent,
491
+ return_targetInst,
492
+ targetContainer,
493
+ );
494
+ resetReplayingEvent();
495
+ }
496
} else {
497
// We're still blocked. Try again later.
498
const fiber = getInstanceFromNode(nextBlockedOn);
@@ -451,6 +520,55 @@ function attemptReplayContinuousQueuedEventInMap(
520
521
function replayUnblockedEvents() {
522
hasScheduledReplayAttempt = false;
523
+ if (!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
524
+ // First replay discrete events.
525
+ while (queuedDiscreteEvents.length > 0) {
526
+ const nextDiscreteEvent = queuedDiscreteEvents[0];
527
+ if (nextDiscreteEvent.blockedOn !== null) {
528
+ // We're still blocked.
529
+ // Increase the priority of this boundary to unblock
530
+ // the next discrete event.
531
+ const fiber = getInstanceFromNode(nextDiscreteEvent.blockedOn);
532
+ if (fiber !== null) {
533
+ attemptDiscreteHydration(fiber);
534
+ }
535
+ break;
536
+ }
537
+ const targetContainers = nextDiscreteEvent.targetContainers;
538
+ while (targetContainers.length > 0) {
539
+ const targetContainer = targetContainers[0];
540
+ const nextBlockedOn = findInstanceBlockingEvent(
541
+ nextDiscreteEvent.domEventName,
542
+ nextDiscreteEvent.eventSystemFlags,
543
+ targetContainer,
544
+ nextDiscreteEvent.nativeEvent,
545
+ );
546
+ if (nextBlockedOn === null) {
547
+ // This whole function is in !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
548
+ // so we don't need the new replay behavior code branch.
549
+ setReplayingEvent(nextDiscreteEvent.nativeEvent);
550
+ dispatchEventForPluginEventSystem(
551
+ nextDiscreteEvent.domEventName,
552
+ nextDiscreteEvent.eventSystemFlags,
553
+ nextDiscreteEvent.nativeEvent,
554
+ return_targetInst,
555
+ targetContainer,
556
+ );
557
+ resetReplayingEvent();
558
+ } else {
559
+ // We're still blocked. Try again later.
560
+ nextDiscreteEvent.blockedOn = nextBlockedOn;
561
+ break;
562
+ }
563
+ // This target container was successfully dispatched. Try the next.
564
+ targetContainers.shift();
565
+ }
566
+ if (nextDiscreteEvent.blockedOn === null) {
567
+ // We've successfully replayed the first event. Let's try the next one.
568
+ queuedDiscreteEvents.shift();
569
+ }
570
+ }
571
+ }
572
// Next replay any continuous events.
573
if (queuedFocus !== null && attemptReplayContinuousQueuedEvent(queuedFocus)) {
574
queuedFocus = null;
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+10
-1
@@ -650,7 +650,16 @@ describe('DOMPluginEventSystem', () => {
650
651
// We're now full hydrated.
652
653
- expect(clicks).toBe(0);
653
+ if (
654
+ gate(
655
+ flags =>
656
+ flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
657
+ )
658
+ ) {
659
+ expect(clicks).toBe(0);
660
+ } else {
661
+ expect(clicks).toBe(1);
662
+ }
663
664
document.body.removeChild(parentContainer);
665
});
packages/react-reconciler/src/ReactFiberReconciler.js
+5
@@ -27,6 +27,7 @@ import {
27
flushPassiveEffects as flushPassiveEffects_old,
28
getPublicRootInstance as getPublicRootInstance_old,
29
attemptSynchronousHydration as attemptSynchronousHydration_old,
30
+ attemptDiscreteHydration as attemptDiscreteHydration_old,
31
attemptContinuousHydration as attemptContinuousHydration_old,
32
attemptHydrationAtCurrentPriority as attemptHydrationAtCurrentPriority_old,
33
findHostInstance as findHostInstance_old,
@@ -64,6 +65,7 @@ import {
65
flushPassiveEffects as flushPassiveEffects_new,
66
getPublicRootInstance as getPublicRootInstance_new,
67
attemptSynchronousHydration as attemptSynchronousHydration_new,
68
+ attemptDiscreteHydration as attemptDiscreteHydration_new,
69
attemptContinuousHydration as attemptContinuousHydration_new,
70
attemptHydrationAtCurrentPriority as attemptHydrationAtCurrentPriority_new,
71
findHostInstance as findHostInstance_new,
@@ -122,6 +124,9 @@ export const getPublicRootInstance = enableNewReconciler
124
export const attemptSynchronousHydration = enableNewReconciler
125
? attemptSynchronousHydration_new
126
: attemptSynchronousHydration_old;
127
+export const attemptDiscreteHydration = enableNewReconciler
128
+ ? attemptDiscreteHydration_new
129
+ : attemptDiscreteHydration_old;
130
export const attemptContinuousHydration = enableNewReconciler
131
? attemptContinuousHydration_new
132
: attemptContinuousHydration_old;
packages/react-reconciler/src/ReactFiberReconciler.new.js
+14
@@ -452,6 +452,20 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) {
452
}
453
}
454
455
+export function attemptDiscreteHydration(fiber: Fiber): void {
456
+ if (fiber.tag !== SuspenseComponent) {
457
+ // We ignore HostRoots here because we can't increase
458
+ // their priority and they should not suspend on I/O,
459
+ // since you have to wrap anything that might suspend in
460
+ // Suspense.
461
+ return;
462
+ }
463
+ const eventTime = requestEventTime();
464
+ const lane = SyncLane;
465
+ scheduleUpdateOnFiber(fiber, lane, eventTime);
466
+ markRetryLaneIfNotHydrated(fiber, lane);
467
+}
468
+
469
export function attemptContinuousHydration(fiber: Fiber): void {
470
if (fiber.tag !== SuspenseComponent) {
471
// We ignore HostRoots here because we can't increase
packages/react-reconciler/src/ReactFiberReconciler.old.js
+14
@@ -452,6 +452,20 @@ function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) {
452
}
453
}
454
455
+export function attemptDiscreteHydration(fiber: Fiber): void {
456
+ if (fiber.tag !== SuspenseComponent) {
457
+ // We ignore HostRoots here because we can't increase
458
+ // their priority and they should not suspend on I/O,
459
+ // since you have to wrap anything that might suspend in
460
+ // Suspense.
461
+ return;
462
+ }
463
+ const eventTime = requestEventTime();
464
+ const lane = SyncLane;
465
+ scheduleUpdateOnFiber(fiber, lane, eventTime);
466
+ markRetryLaneIfNotHydrated(fiber, lane);
467
+}
468
+
469
export function attemptContinuousHydration(fiber: Fiber): void {
470
if (fiber.tag !== SuspenseComponent) {
471
// We ignore HostRoots here because we can't increase
packages/shared/ReactFeatureFlags.js
+3
@@ -44,6 +44,9 @@ export const enableSuspenseLayoutEffectSemantics = true;
44
export const enableClientRenderFallbackOnHydrationMismatch = true;
45
export const enableClientRenderFallbackOnTextMismatch = true;
46
47
+// TODO: Need to review this code one more time before landing
48
+export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
49
+
50
// Recoil still uses useMutableSource in www, need to delete
51
export const enableUseMutableSource = false;
52
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -52,6 +52,7 @@ export const warnAboutSpreadingKeyToJSX = false;
52
export const enableSuspenseAvoidThisFallback = false;
53
export const enableSuspenseAvoidThisFallbackFizz = false;
54
export const enableCPUSuspense = true;
55
+export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
56
export const enableClientRenderFallbackOnHydrationMismatch = true;
57
export const enableClientRenderFallbackOnTextMismatch = true;
58
export const enableComponentStackLocations = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -43,6 +43,7 @@ export const warnAboutSpreadingKeyToJSX = false;
43
export const enableSuspenseAvoidThisFallback = false;
44
export const enableSuspenseAvoidThisFallbackFizz = false;
45
export const enableCPUSuspense = false;
46
+export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
47
export const enableClientRenderFallbackOnHydrationMismatch = true;
48
export const enableClientRenderFallbackOnTextMismatch = true;
49
export const enableComponentStackLocations = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -43,6 +43,7 @@ export const warnAboutSpreadingKeyToJSX = false;
43
export const enableSuspenseAvoidThisFallback = false;
44
export const enableSuspenseAvoidThisFallbackFizz = false;
45
export const enableCPUSuspense = false;
46
+export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
47
export const enableClientRenderFallbackOnHydrationMismatch = true;
48
export const enableClientRenderFallbackOnTextMismatch = true;
49
export const enableComponentStackLocations = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -53,6 +53,7 @@ export const deferRenderPhaseUpdateToNextBatch = false;
53
export const enableSuspenseAvoidThisFallback = false;
54
export const enableSuspenseAvoidThisFallbackFizz = false;
55
export const enableCPUSuspense = false;
56
+export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
57
export const enableClientRenderFallbackOnHydrationMismatch = true;
58
export const enableClientRenderFallbackOnTextMismatch = true;
59
export const enableStrictEffects = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -43,6 +43,7 @@ export const warnAboutSpreadingKeyToJSX = false;
43
export const enableSuspenseAvoidThisFallback = true;
44
export const enableSuspenseAvoidThisFallbackFizz = false;
45
export const enableCPUSuspense = false;
46
+export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
47
export const enableClientRenderFallbackOnHydrationMismatch = true;
48
export const enableClientRenderFallbackOnTextMismatch = true;
49
export const enableComponentStackLocations = true;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -43,6 +43,7 @@ export const warnAboutSpreadingKeyToJSX = false;
43
export const enableSuspenseAvoidThisFallback = false;
44
export const enableSuspenseAvoidThisFallbackFizz = false;
45
export const enableCPUSuspense = false;
46
+export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
47
export const enableClientRenderFallbackOnHydrationMismatch = true;
48
export const enableClientRenderFallbackOnTextMismatch = true;
49
export const enableComponentStackLocations = true;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -43,6 +43,7 @@ export const warnAboutSpreadingKeyToJSX = false;
43
export const enableSuspenseAvoidThisFallback = true;
44
export const enableSuspenseAvoidThisFallbackFizz = false;
45
export const enableCPUSuspense = true;
46
+export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
47
export const enableClientRenderFallbackOnHydrationMismatch = true;
48
export const enableClientRenderFallbackOnTextMismatch = true;
49
export const enableComponentStackLocations = true;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -25,6 +25,7 @@ export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
25
export const enableLazyContextPropagation = __VARIANT__;
26
export const enableSyncDefaultUpdates = __VARIANT__;
27
export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
28
+export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__;
29
export const enableClientRenderFallbackOnHydrationMismatch = __VARIANT__;
30
export const enableClientRenderFallbackOnTextMismatch = __VARIANT__;
31
export const enableTransitionTracing = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -31,6 +31,7 @@ export const {
31
disableSchedulerTimeoutInWorkLoop,
32
enableLazyContextPropagation,
33
enableSyncDefaultUpdates,
34
+ enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
35
enableClientRenderFallbackOnHydrationMismatch,
36
enableClientRenderFallbackOnTextMismatch,
37
} = dynamicFeatureFlags;