@samitouri / QOS-React-2 / commits / a6d53f3468

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;