@samitouri / QOS-React-2 / commits / 0ecbbe1422

Sync hydrate discrete events in capture phase and dont replay discrete events (#22448)

salazarm committed Oct 4, 2021 at 09:54 UTC 0ecbbe142201ef65830f0b7a19d52c1f77043e86
15 files changed +402 -102
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+80 -13
@@ -1905,10 +1905,19 @@ describe('ReactDOMServerPartialHydration', () => {
1905 resolve();
1906 await promise;
1907 });
1908 - expect(clicks).toBe(1);
1909 -
1910 - expect(container.textContent).toBe('Hello');
1908
1909 + if (
1910 + gate(
1911 + flags =>
1912 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
1913 + )
1914 + ) {
1915 + expect(clicks).toBe(0);
1916 + expect(container.textContent).toBe('Click meHello');
1917 + } else {
1918 + expect(clicks).toBe(1);
1919 + expect(container.textContent).toBe('Hello');
1920 + }
1921 document.body.removeChild(container);
1922 });
1923
@@ -1991,7 +2000,16 @@ describe('ReactDOMServerPartialHydration', () => {
2000 await promise;
2001 });
2002
1994 - expect(onEvent).toHaveBeenCalledTimes(2);
2003 + if (
2004 + gate(
2005 + flags =>
2006 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2007 + )
2008 + ) {
2009 + expect(onEvent).toHaveBeenCalledTimes(0);
2010 + } else {
2011 + expect(onEvent).toHaveBeenCalledTimes(2);
2012 + }
2013
2014 document.body.removeChild(container);
2015 });
@@ -2072,7 +2090,17 @@ describe('ReactDOMServerPartialHydration', () => {
2090 resolve();
2091 await promise;
2092 });
2075 - expect(clicks).toBe(2);
2093 +
2094 + if (
2095 + gate(
2096 + flags =>
2097 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2098 + )
2099 + ) {
2100 + expect(clicks).toBe(0);
2101 + } else {
2102 + expect(clicks).toBe(2);
2103 + }
2104
2105 document.body.removeChild(container);
2106 });
@@ -2158,7 +2186,16 @@ describe('ReactDOMServerPartialHydration', () => {
2186 resolve();
2187 await promise;
2188 });
2161 - expect(onEvent).toHaveBeenCalledTimes(2);
2189 + if (
2190 + gate(
2191 + flags =>
2192 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2193 + )
2194 + ) {
2195 + expect(onEvent).toHaveBeenCalledTimes(0);
2196 + } else {
2197 + expect(onEvent).toHaveBeenCalledTimes(2);
2198 + }
2199
2200 document.body.removeChild(container);
2201 });
@@ -2231,9 +2268,19 @@ describe('ReactDOMServerPartialHydration', () => {
2268 await promise;
2269 });
2270
2234 - expect(clicksOnChild).toBe(1);
2235 - // This will be zero due to the stopPropagation.
2236 - expect(clicksOnParent).toBe(0);
2271 + if (
2272 + gate(
2273 + flags =>
2274 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2275 + )
2276 + ) {
2277 + expect(clicksOnChild).toBe(0);
2278 + expect(clicksOnParent).toBe(0);
2279 + } else {
2280 + expect(clicksOnChild).toBe(1);
2281 + // This will be zero due to the stopPropagation.
2282 + expect(clicksOnParent).toBe(0);
2283 + }
2284
2285 document.body.removeChild(container);
2286 });
@@ -2310,8 +2357,16 @@ describe('ReactDOMServerPartialHydration', () => {
2357 });
2358
2359 // We're now full hydrated.
2313 -
2314 - expect(clicks).toBe(1);
2360 + if (
2361 + gate(
2362 + flags =>
2363 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2364 + )
2365 + ) {
2366 + expect(clicks).toBe(0);
2367 + } else {
2368 + expect(clicks).toBe(1);
2369 + }
2370
2371 document.body.removeChild(parentContainer);
2372 });
@@ -2580,8 +2635,20 @@ describe('ReactDOMServerPartialHydration', () => {
2635 await promise;
2636 });
2637
2583 - expect(submits).toBe(1);
2584 - expect(container.textContent).toBe('Hello');
2638 + if (
2639 + gate(
2640 + flags =>
2641 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
2642 + )
2643 + ) {
2644 + // discrete event not replayed
2645 + expect(submits).toBe(0);
2646 + expect(container.textContent).toBe('Click meHello');
2647 + } else {
2648 + expect(submits).toBe(1);
2649 + expect(container.textContent).toBe('Hello');
2650 + }
2651 +
2652 document.body.removeChild(container);
2653 });
2654
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+210 -50
@@ -14,6 +14,7 @@ import {createEventTarget} from 'dom-event-testing-library';
14 let React;
15 let ReactDOM;
16 let ReactDOMServer;
17 +let ReactFeatureFlags;
18 let Scheduler;
19 let Suspense;
20 let act;
@@ -112,7 +113,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
113 beforeEach(() => {
114 jest.resetModuleRegistry();
115
115 - const ReactFeatureFlags = require('shared/ReactFeatureFlags');
116 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
117 ReactFeatureFlags.enableCreateEventHandleAPI = true;
118 React = require('react');
119 ReactDOM = require('react-dom');
@@ -266,9 +267,19 @@ describe('ReactDOMServerSelectiveHydration', () => {
267 resolve();
268 await promise;
269 });
269 - // After the click, we should prioritize D and the Click first,
270 - // and only after that render A and C.
271 - expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']);
270 +
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 });
@@ -343,7 +354,16 @@ describe('ReactDOMServerSelectiveHydration', () => {
354 dispatchClickEvent(spanC);
355 dispatchClickEvent(spanD);
356
346 - expect(Scheduler).toHaveYielded(['App']);
357 + if (
358 + gate(
359 + flags =>
360 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
361 + )
362 + ) {
363 + expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']);
364 + } else {
365 + expect(Scheduler).toHaveYielded(['App']);
366 + }
367
368 await act(async () => {
369 suspend = false;
@@ -351,18 +371,29 @@ describe('ReactDOMServerSelectiveHydration', () => {
371 await promise;
372 });
373
354 - // We should prioritize hydrating A, C and D first since we clicked in
355 - // them. Only after they're done will we hydrate B.
356 - expect(Scheduler).toHaveYielded([
357 - 'A',
358 - 'Clicked A',
359 - 'C',
360 - 'Clicked C',
361 - 'D',
362 - 'Clicked D',
363 - // B should render last since it wasn't clicked.
364 - 'B',
365 - ]);
374 + if (
375 + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
376 + ) {
377 + expect(Scheduler).toHaveYielded([
378 + 'A',
379 + 'D',
380 + // B should render last since it wasn't clicked.
381 + 'B',
382 + ]);
383 + } else {
384 + // We should prioritize hydrating A, C and D first since we clicked in
385 + // them. Only after they're done will we hydrate B.
386 + expect(Scheduler).toHaveYielded([
387 + 'A',
388 + 'Clicked A',
389 + 'C',
390 + 'Clicked C',
391 + 'D',
392 + 'Clicked D',
393 + // B should render last since it wasn't clicked.
394 + 'B',
395 + ]);
396 + }
397
398 document.body.removeChild(container);
399 });
@@ -519,7 +550,17 @@ describe('ReactDOMServerSelectiveHydration', () => {
550 resolve();
551 await promise;
552 });
522 - expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']);
553 + if (
554 + gate(
555 + flags =>
556 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
557 + )
558 + ) {
559 + // no replay
560 + expect(Scheduler).toHaveYielded(['D', 'A']);
561 + } else {
562 + expect(Scheduler).toHaveYielded(['D', 'Clicked D', 'A']);
563 + }
564
565 document.body.removeChild(container);
566 });
@@ -599,26 +640,42 @@ describe('ReactDOMServerSelectiveHydration', () => {
640 createEventTarget(spanC).virtualclick();
641 createEventTarget(spanD).virtualclick();
642
602 - expect(Scheduler).toHaveYielded(['App']);
603 -
643 + if (
644 + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
645 + ) {
646 + expect(Scheduler).toHaveYielded(['App', 'C', 'Clicked C']);
647 + } else {
648 + expect(Scheduler).toHaveYielded(['App']);
649 + }
650 await act(async () => {
651 suspend = false;
652 resolve();
653 await promise;
654 });
655
610 - // We should prioritize hydrating A, C and D first since we clicked in
611 - // them. Only after they're done will we hydrate B.
612 - expect(Scheduler).toHaveYielded([
613 - 'A',
614 - 'Clicked A',
615 - 'C',
616 - 'Clicked C',
617 - 'D',
618 - 'Clicked D',
619 - // B should render last since it wasn't clicked.
620 - 'B',
621 - ]);
656 + if (
657 + ReactFeatureFlags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
658 + ) {
659 + expect(Scheduler).toHaveYielded([
660 + 'A',
661 + 'D',
662 + // B should render last since it wasn't clicked.
663 + 'B',
664 + ]);
665 + } else {
666 + // We should prioritize hydrating A, C and D first since we clicked in
667 + // them. Only after they're done will we hydrate B.
668 + expect(Scheduler).toHaveYielded([
669 + 'A',
670 + 'Clicked A',
671 + 'C',
672 + 'Clicked C',
673 + 'D',
674 + 'Clicked D',
675 + // B should render last since it wasn't clicked.
676 + 'B',
677 + ]);
678 + }
679
680 document.body.removeChild(container);
681 });
@@ -707,20 +764,37 @@ describe('ReactDOMServerSelectiveHydration', () => {
764 await promise;
765 });
766
710 - // We should prioritize hydrating D first because we clicked it.
711 - // Next we should hydrate C since that's the current hover target.
712 - // To simplify implementation details we hydrate both B and C at
713 - // the same time since B was already scheduled.
714 - // This is ok because it will at least not continue for nested
715 - // boundary. See the next test below.
716 - expect(Scheduler).toHaveYielded([
717 - 'D',
718 - 'Clicked D',
719 - 'B', // Ideally this should be later.
720 - 'C',
721 - 'Hover C',
722 - 'A',
723 - ]);
767 + if (
768 + gate(
769 + flags =>
770 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
771 + )
772 + ) {
773 + // We should prioritize hydrating D first because we clicked it.
774 + // but event isnt replayed
775 + expect(Scheduler).toHaveYielded([
776 + 'D',
777 + 'B', // Ideally this should be later.
778 + 'C',
779 + 'Hover C',
780 + 'A',
781 + ]);
782 + } else {
783 + // We should prioritize hydrating D first because we clicked it.
784 + // Next we should hydrate C since that's the current hover target.
785 + // To simplify implementation details we hydrate both B and C at
786 + // the same time since B was already scheduled.
787 + // This is ok because it will at least not continue for nested
788 + // boundary. See the next test below.
789 + expect(Scheduler).toHaveYielded([
790 + 'D',
791 + 'Clicked D',
792 + 'B', // Ideally this should be later.
793 + 'C',
794 + 'Hover C',
795 + 'A',
796 + ]);
797 + }
798
799 document.body.removeChild(container);
800 });
@@ -796,16 +870,18 @@ describe('ReactDOMServerSelectiveHydration', () => {
870 dispatchMouseHoverEvent(spanB, spanD);
871 dispatchMouseHoverEvent(spanC, spanB);
872
799 - suspend = false;
800 - resolve();
801 - await promise;
873 + await act(async () => {
874 + suspend = false;
875 + resolve();
876 + await promise;
877 + });
878
879 // We should prioritize hydrating D first because we clicked it.
880 // Next we should hydrate C since that's the current hover target.
881 // Next it doesn't matter if we hydrate A or B first but as an
882 // implementation detail we're currently hydrating B first since
883 // we at one point hovered over it and we never deprioritized it.
808 - expect(Scheduler).toFlushAndYield(['App', 'C', 'Hover C', 'A', 'B', 'D']);
884 + expect(Scheduler).toHaveYielded(['App', 'C', 'Hover C', 'A', 'B', 'D']);
885
886 document.body.removeChild(container);
887 });
@@ -970,4 +1046,88 @@ describe('ReactDOMServerSelectiveHydration', () => {
1046
1047 document.body.removeChild(container);
1048 });
1049 +
1050 + // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1051 + it('fires capture event handlers and native events if content is hydratable during discrete event', async () => {
1052 + spyOnDev(console, 'error');
1053 + function Child({text}) {
1054 + Scheduler.unstable_yieldValue(text);
1055 + const ref = React.useRef();
1056 + React.useLayoutEffect(() => {
1057 + if (!ref.current) {
1058 + return;
1059 + }
1060 + ref.current.onclick = () => {
1061 + Scheduler.unstable_yieldValue('Native Click ' + text);
1062 + };
1063 + }, [text]);
1064 + return (
1065 + <span
1066 + ref={ref}
1067 + onClickCapture={() => {
1068 + Scheduler.unstable_yieldValue('Capture Clicked ' + text);
1069 + }}
1070 + onClick={e => {
1071 + Scheduler.unstable_yieldValue('Clicked ' + text);
1072 + }}>
1073 + {text}
1074 + </span>
1075 + );
1076 + }
1077 +
1078 + function App() {
1079 + Scheduler.unstable_yieldValue('App');
1080 + return (
1081 + <div>
1082 + <Suspense fallback="Loading...">
1083 + <Child text="A" />
1084 + </Suspense>
1085 + <Suspense fallback="Loading...">
1086 + <Child text="B" />
1087 + </Suspense>
1088 + </div>
1089 + );
1090 + }
1091 +
1092 + let finalHTML;
1093 + expect(() => {
1094 + finalHTML = ReactDOMServer.renderToString(<App />);
1095 + }).toErrorDev([
1096 + 'useLayoutEffect does nothing on the server',
1097 + 'useLayoutEffect does nothing on the server',
1098 + ]);
1099 +
1100 + expect(Scheduler).toHaveYielded(['App', 'A', 'B']);
1101 +
1102 + const container = document.createElement('div');
1103 + // We need this to be in the document since we'll dispatch events on it.
1104 + document.body.appendChild(container);
1105 +
1106 + container.innerHTML = finalHTML;
1107 +
1108 + const span = container.getElementsByTagName('span')[1];
1109 +
1110 + ReactDOM.hydrateRoot(container, <App />);
1111 +
1112 + // Nothing has been hydrated so far.
1113 + expect(Scheduler).toHaveYielded([]);
1114 +
1115 + // This should synchronously hydrate the root App and the second suspense
1116 + // boundary.
1117 + dispatchClickEvent(span);
1118 +
1119 + // We rendered App, B and then invoked the event without rendering A.
1120 + expect(Scheduler).toHaveYielded([
1121 + 'App',
1122 + 'B',
1123 + 'Capture Clicked B',
1124 + 'Native Click B',
1125 + 'Clicked B',
1126 + ]);
1127 +
1128 + // After continuing the scheduler, we finally hydrate A.
1129 + expect(Scheduler).toFlushAndYield(['A']);
1130 +
1131 + document.body.removeChild(container);
1132 + });
1133 });
packages/react-dom/src/events/ReactDOMEventListener.js
+38 -4
@@ -11,13 +11,18 @@ 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 -
14 +import {
15 + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
16 + enableSelectiveHydration,
17 +} from 'shared/ReactFeatureFlags';
18 import {
19 isReplayableDiscreteEvent,
20 queueDiscreteEvent,
21 hasQueuedDiscreteEvents,
22 clearIfContinuousEvent,
23 queueIfContinuousEvent,
24 + attemptSynchronousHydration,
25 + isCapturePhaseSynchronouslyHydratableEvent,
26 } from './ReactDOMEventReplaying';
27 import {
28 getNearestMountedFiber,
@@ -28,7 +33,10 @@ import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
33 import {type EventSystemFlags, IS_CAPTURE_PHASE} from './EventSystemFlags';
34
35 import getEventTarget from './getEventTarget';
31 -import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
36 +import {
37 + getInstanceFromNode,
38 + getClosestInstanceFromNode,
39 +} from '../client/ReactDOMComponentTree';
40
41 import {dispatchEventForPluginEventSystem} from './DOMPluginEventSystem';
42
@@ -176,7 +184,7 @@ export function dispatchEvent(
184 return;
185 }
186
179 - const blockedOn = attemptToDispatchEvent(
187 + let blockedOn = attemptToDispatchEvent(
188 domEventName,
189 eventSystemFlags,
190 targetContainer,
@@ -192,7 +200,10 @@ export function dispatchEvent(
200 }
201
202 if (allowReplay) {
195 - if (isReplayableDiscreteEvent(domEventName)) {
203 + if (
204 + !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
205 + isReplayableDiscreteEvent(domEventName)
206 + ) {
207 // This this to be replayed later once the target is available.
208 queueDiscreteEvent(
209 blockedOn,
@@ -219,6 +230,29 @@ export function dispatchEvent(
230 clearIfContinuousEvent(domEventName, nativeEvent);
231 }
232
233 + if (
234 + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
235 + enableSelectiveHydration &&
236 + isCapturePhaseSynchronouslyHydratableEvent(domEventName)
237 + ) {
238 + while (blockedOn !== null) {
239 + const fiber = getInstanceFromNode(blockedOn);
240 + if (fiber !== null) {
241 + attemptSynchronousHydration(fiber);
242 + }
243 + const nextBlockedOn = attemptToDispatchEvent(
244 + domEventName,
245 + eventSystemFlags,
246 + targetContainer,
247 + nativeEvent,
248 + );
249 + if (nextBlockedOn === blockedOn) {
250 + break;
251 + }
252 + blockedOn = nextBlockedOn;
253 + }
254 + }
255 +
256 // This is not replayable so we'll invoke it but without a target,
257 // in case the event system needs to trace it.
258 dispatchEventForPluginEventSystem(
packages/react-dom/src/events/ReactDOMEventReplaying.js
+53 -34
@@ -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,
@@ -32,10 +35,14 @@ import {
35 import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
36 import {isHigherEventPriority} from 'react-reconciler/src/ReactEventPriorities';
37
35 -let attemptSynchronousHydration: (fiber: Object) => void;
38 +let _attemptSynchronousHydration: (fiber: Object) => void;
39
40 export function setAttemptSynchronousHydration(fn: (fiber: Object) => void) {
38 - attemptSynchronousHydration = fn;
41 + _attemptSynchronousHydration = fn;
42 +}
43 +
44 +export function attemptSynchronousHydration(fiber: Object) {
45 + _attemptSynchronousHydration(fiber);
46 }
47
48 let attemptDiscreteHydration: (fiber: Object) => void;
@@ -180,6 +187,9 @@ export function queueDiscreteEvent(
187 targetContainer: EventTarget,
188 nativeEvent: AnyNativeEvent,
189 ): void {
190 + if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
191 + return;
192 + }
193 const queuedEvent = createQueuedReplayableEvent(
194 blockedOn,
195 domEventName,
@@ -290,6 +300,13 @@ function accumulateOrCreateContinuousQueuedReplayableEvent(
300 return existingQueuedEvent;
301 }
302
303 +export function isCapturePhaseSynchronouslyHydratableEvent(
304 + eventName: DOMEventName,
305 +) {
306 + // TODO: maybe include more events
307 + return isReplayableDiscreteEvent(eventName);
308 +}
309 +
310 export function queueIfContinuousEvent(
311 blockedOn: null | Container | SuspenseInstance,
312 domEventName: DOMEventName,
@@ -483,39 +500,41 @@ function attemptReplayContinuousQueuedEventInMap(
500
501 function replayUnblockedEvents() {
502 hasScheduledReplayAttempt = false;
486 - // First replay discrete events.
487 - while (queuedDiscreteEvents.length > 0) {
488 - const nextDiscreteEvent = queuedDiscreteEvents[0];
489 - if (nextDiscreteEvent.blockedOn !== null) {
490 - // We're still blocked.
491 - // Increase the priority of this boundary to unblock
492 - // the next discrete event.
493 - const fiber = getInstanceFromNode(nextDiscreteEvent.blockedOn);
494 - if (fiber !== null) {
495 - attemptDiscreteHydration(fiber);
496 - }
497 - break;
498 - }
499 - const targetContainers = nextDiscreteEvent.targetContainers;
500 - while (targetContainers.length > 0) {
501 - const targetContainer = targetContainers[0];
502 - const nextBlockedOn = attemptToDispatchEvent(
503 - nextDiscreteEvent.domEventName,
504 - nextDiscreteEvent.eventSystemFlags,
505 - targetContainer,
506 - nextDiscreteEvent.nativeEvent,
507 - );
508 - if (nextBlockedOn !== null) {
509 - // We're still blocked. Try again later.
510 - nextDiscreteEvent.blockedOn = nextBlockedOn;
503 + if (!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
504 + // First replay discrete events.
505 + while (queuedDiscreteEvents.length > 0) {
506 + const nextDiscreteEvent = queuedDiscreteEvents[0];
507 + if (nextDiscreteEvent.blockedOn !== null) {
508 + // We're still blocked.
509 + // Increase the priority of this boundary to unblock
510 + // the next discrete event.
511 + const fiber = getInstanceFromNode(nextDiscreteEvent.blockedOn);
512 + if (fiber !== null) {
513 + attemptDiscreteHydration(fiber);
514 + }
515 break;
516 }
513 - // This target container was successfully dispatched. Try the next.
514 - targetContainers.shift();
515 - }
516 - if (nextDiscreteEvent.blockedOn === null) {
517 - // We've successfully replayed the first event. Let's try the next one.
518 - queuedDiscreteEvents.shift();
517 + const targetContainers = nextDiscreteEvent.targetContainers;
518 + while (targetContainers.length > 0) {
519 + const targetContainer = targetContainers[0];
520 + const nextBlockedOn = attemptToDispatchEvent(
521 + nextDiscreteEvent.domEventName,
522 + nextDiscreteEvent.eventSystemFlags,
523 + targetContainer,
524 + nextDiscreteEvent.nativeEvent,
525 + );
526 + if (nextBlockedOn !== null) {
527 + // We're still blocked. Try again later.
528 + nextDiscreteEvent.blockedOn = nextBlockedOn;
529 + break;
530 + }
531 + // This target container was successfully dispatched. Try the next.
532 + targetContainers.shift();
533 + }
534 + if (nextDiscreteEvent.blockedOn === null) {
535 + // We've successfully replayed the first event. Let's try the next one.
536 + queuedDiscreteEvents.shift();
537 + }
538 }
539 }
540 // Next replay any continuous events.
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+10 -1
@@ -649,7 +649,16 @@ describe('DOMPluginEventSystem', () => {
649
650 // We're now full hydrated.
651
652 - expect(clicks).toBe(1);
652 + if (
653 + gate(
654 + flags =>
655 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
656 + )
657 + ) {
658 + expect(clicks).toBe(0);
659 + } else {
660 + expect(clicks).toBe(1);
661 + }
662
663 document.body.removeChild(parentContainer);
664 });
packages/shared/ReactFeatureFlags.js
+2
@@ -103,6 +103,8 @@ export const warnOnSubscriptionInsideStartTransition = false;
103
104 export const enableSuspenseAvoidThisFallback = false;
105
106 +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
107 +
108 export const enableComponentStackLocations = true;
109
110 export const enableNewReconciler = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -50,6 +50,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
50 export const warnAboutSpreadingKeyToJSX = false;
51 export const warnOnSubscriptionInsideStartTransition = false;
52 export const enableSuspenseAvoidThisFallback = false;
53 +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
54 export const enableComponentStackLocations = false;
55 export const enableLegacyFBSupport = false;
56 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
41 export const warnAboutSpreadingKeyToJSX = false;
42 export const warnOnSubscriptionInsideStartTransition = false;
43 export const enableSuspenseAvoidThisFallback = false;
44 +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45 export const enableComponentStackLocations = false;
46 export const enableLegacyFBSupport = false;
47 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
41 export const warnAboutSpreadingKeyToJSX = false;
42 export const warnOnSubscriptionInsideStartTransition = false;
43 export const enableSuspenseAvoidThisFallback = false;
44 +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45 export const enableComponentStackLocations = true;
46 export const enableLegacyFBSupport = false;
47 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -51,6 +51,7 @@ export const enableNewReconciler = false;
51 export const deferRenderPhaseUpdateToNextBatch = false;
52 export const warnOnSubscriptionInsideStartTransition = false;
53 export const enableSuspenseAvoidThisFallback = false;
54 +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
55 export const enableStrictEffects = false;
56 export const createRootStrictEffectsByDefault = false;
57 export const enableUseRefAccessWarning = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
41 export const warnAboutSpreadingKeyToJSX = false;
42 export const warnOnSubscriptionInsideStartTransition = false;
43 export const enableSuspenseAvoidThisFallback = false;
44 +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45 export const enableComponentStackLocations = true;
46 export const enableLegacyFBSupport = false;
47 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
41 export const warnAboutSpreadingKeyToJSX = false;
42 export const warnOnSubscriptionInsideStartTransition = false;
43 export const enableSuspenseAvoidThisFallback = false;
44 +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45 export const enableComponentStackLocations = true;
46 export const enableLegacyFBSupport = false;
47 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -41,6 +41,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false;
41 export const warnAboutSpreadingKeyToJSX = false;
42 export const warnOnSubscriptionInsideStartTransition = false;
43 export const enableSuspenseAvoidThisFallback = false;
44 +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
45 export const enableComponentStackLocations = true;
46 export const enableLegacyFBSupport = !__EXPERIMENTAL__;
47 export const enableFilterEmptyStringAttributesDOM = false;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -27,6 +27,7 @@ export const enableLazyContextPropagation = __VARIANT__;
27 export const enableSyncDefaultUpdates = __VARIANT__;
28 export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
29 export const warnOnSubscriptionInsideStartTransition = __VARIANT__;
30 +export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__;
31
32 // Enable this flag to help with concurrent mode debugging.
33 // It logs information to the console about React scheduling, rendering, and commit phases.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -32,6 +32,7 @@ export const {
32 enableLazyContextPropagation,
33 enableSyncDefaultUpdates,
34 warnOnSubscriptionInsideStartTransition,
35 + enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
36 } = dynamicFeatureFlags;
37
38 // On WWW, __EXPERIMENTAL__ is used for a new modern build.