@samitouri / QOS-React-2 / commits / 71d16750c5

Replay capture phase for continuous events (#22856)

Co-authored-by: Dan Abramov <dan.abramov@me.com> Co-authored-by: Marco Salazar <salazarm@fb.com>

Dan Abramov committed Dec 2, 2021 at 14:33 UTC 71d16750c528ad902e73e2ecb9fc05a40314bb3f
3 files changed +558 -60
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+519 -6
@@ -684,7 +684,6 @@ describe('ReactDOMServerSelectiveHydration', () => {
684 let suspend = false;
685 let resolve;
686 const promise = new Promise(resolvePromise => (resolve = resolvePromise));
687 -
687 function Child({text}) {
688 if ((text === 'A' || text === 'D') && suspend) {
689 throw promise;
@@ -724,11 +723,8 @@ describe('ReactDOMServerSelectiveHydration', () => {
723 </div>
724 );
725 }
727 -
726 const finalHTML = ReactDOMServer.renderToString(<App />);
729 -
727 expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
731 -
728 const container = document.createElement('div');
729 // We need this to be in the document since we'll dispatch events on it.
730 document.body.appendChild(container);
@@ -746,6 +742,156 @@ describe('ReactDOMServerSelectiveHydration', () => {
742 const root = ReactDOM.createRoot(container, {hydrate: true});
743 root.render(<App />);
744
745 + // Nothing has been hydrated so far.
746 + expect(Scheduler).toHaveYielded([]);
747 + // Click D
748 + dispatchMouseHoverEvent(spanD, null);
749 + dispatchClickEvent(spanD);
750 + // Hover over B and then C.
751 + dispatchMouseHoverEvent(spanB, spanD);
752 + dispatchMouseHoverEvent(spanC, spanB);
753 + expect(Scheduler).toHaveYielded(['App']);
754 + await act(async () => {
755 + suspend = false;
756 + resolve();
757 + await promise;
758 + });
759 + if (
760 + gate(
761 + flags =>
762 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
763 + )
764 + ) {
765 + // We should prioritize hydrating D first because we clicked it.
766 + // but event isnt replayed
767 + expect(Scheduler).toHaveYielded([
768 + 'D',
769 + 'B', // Ideally this should be later.
770 + 'C',
771 + 'Hover C',
772 + 'A',
773 + ]);
774 + } else {
775 + // We should prioritize hydrating D first because we clicked it.
776 + // Next we should hydrate C since that's the current hover target.
777 + // To simplify implementation details we hydrate both B and C at
778 + // the same time since B was already scheduled.
779 + // This is ok because it will at least not continue for nested
780 + // boundary. See the next test below.
781 + expect(Scheduler).toHaveYielded([
782 + 'D',
783 + 'Clicked D',
784 + 'B', // Ideally this should be later.
785 + 'C',
786 + 'Hover C',
787 + 'A',
788 + ]);
789 + }
790 +
791 + document.body.removeChild(container);
792 + });
793 +
794 + it('replays capture phase for continuous events and respects stopPropagation', async () => {
795 + let suspend = false;
796 + let resolve;
797 + const promise = new Promise(resolvePromise => (resolve = resolvePromise));
798 +
799 + function Child({text}) {
800 + if ((text === 'A' || text === 'D') && suspend) {
801 + throw promise;
802 + }
803 + Scheduler.unstable_yieldValue(text);
804 + return (
805 + <span
806 + id={text}
807 + onClickCapture={e => {
808 + e.preventDefault();
809 + Scheduler.unstable_yieldValue('Capture Clicked ' + text);
810 + }}
811 + onClick={e => {
812 + e.preventDefault();
813 + Scheduler.unstable_yieldValue('Clicked ' + text);
814 + }}
815 + onMouseEnter={e => {
816 + e.preventDefault();
817 + Scheduler.unstable_yieldValue('Mouse Enter ' + text);
818 + }}
819 + onMouseOut={e => {
820 + e.preventDefault();
821 + Scheduler.unstable_yieldValue('Mouse Out ' + text);
822 + }}
823 + onMouseOutCapture={e => {
824 + e.preventDefault();
825 + e.stopPropagation();
826 + Scheduler.unstable_yieldValue('Mouse Out Capture ' + text);
827 + }}
828 + onMouseOverCapture={e => {
829 + e.preventDefault();
830 + e.stopPropagation();
831 + Scheduler.unstable_yieldValue('Mouse Over Capture ' + text);
832 + }}
833 + onMouseOver={e => {
834 + e.preventDefault();
835 + Scheduler.unstable_yieldValue('Mouse Over ' + text);
836 + }}>
837 + <div
838 + onMouseOverCapture={e => {
839 + e.preventDefault();
840 + Scheduler.unstable_yieldValue('Mouse Over Capture Inner ' + text);
841 + }}>
842 + {text}
843 + </div>
844 + </span>
845 + );
846 + }
847 +
848 + function App() {
849 + Scheduler.unstable_yieldValue('App');
850 + return (
851 + <div
852 + onClickCapture={e => {
853 + e.preventDefault();
854 + Scheduler.unstable_yieldValue('Capture Clicked Parent');
855 + }}
856 + onMouseOverCapture={e => {
857 + Scheduler.unstable_yieldValue('Mouse Over Capture Parent');
858 + }}>
859 + <Suspense fallback="Loading...">
860 + <Child text="A" />
861 + </Suspense>
862 + <Suspense fallback="Loading...">
863 + <Child text="B" />
864 + </Suspense>
865 + <Suspense fallback="Loading...">
866 + <Child text="C" />
867 + </Suspense>
868 + <Suspense fallback="Loading...">
869 + <Child text="D" />
870 + </Suspense>
871 + </div>
872 + );
873 + }
874 +
875 + const finalHTML = ReactDOMServer.renderToString(<App />);
876 +
877 + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);
878 +
879 + const container = document.createElement('div');
880 + // We need this to be in the document since we'll dispatch events on it.
881 + document.body.appendChild(container);
882 +
883 + container.innerHTML = finalHTML;
884 +
885 + const spanB = document.getElementById('B').firstChild;
886 + const spanC = document.getElementById('C').firstChild;
887 + const spanD = document.getElementById('D').firstChild;
888 +
889 + suspend = true;
890 +
891 + // A and D will be suspended. We'll click on D which should take
892 + // priority, after we unsuspend.
893 + ReactDOM.hydrateRoot(container, <App />);
894 +
895 // Nothing has been hydrated so far.
896 expect(Scheduler).toHaveYielded([]);
897
@@ -776,7 +922,14 @@ describe('ReactDOMServerSelectiveHydration', () => {
922 'D',
923 'B', // Ideally this should be later.
924 'C',
779 - 'Hover C',
925 + // Mouse out events aren't replayed
926 + // 'Mouse Out Capture B',
927 + // 'Mouse Out B',
928 + 'Mouse Over Capture Parent',
929 + 'Mouse Over Capture C',
930 + // Stop propagation stops these
931 + // 'Mouse Over Capture Inner C',
932 + // 'Mouse Over C',
933 'A',
934 ]);
935 } else {
@@ -791,11 +944,371 @@ describe('ReactDOMServerSelectiveHydration', () => {
944 'Clicked D',
945 'B', // Ideally this should be later.
946 'C',
794 - 'Hover C',
947 + // Capture phase isn't replayed
948 + // Mouseout isn't replayed
949 + 'Mouse Over C',
950 + 'Mouse Enter C',
951 'A',
952 ]);
953 }
954
955 + // This test shows existing quirk where stopPropagation on mouseout
956 + // prevents mouseEnter from firing
957 + dispatchMouseHoverEvent(spanC, spanB);
958 + expect(Scheduler).toHaveYielded([
959 + 'Mouse Out Capture B',
960 + // stopPropagation stops these
961 + // 'Mouse Out B',
962 + // 'Mouse Enter C',
963 + 'Mouse Over Capture Parent',
964 + 'Mouse Over Capture C',
965 + // Stop propagation stops these
966 + // 'Mouse Over Capture Inner C',
967 + // 'Mouse Over C',
968 + ]);
969 +
970 + document.body.removeChild(container);
971 + });
972 +
973 + describe('can handle replaying events as part of multiple instances of React', () => {
974 + let resolveInner;
975 + let resolveOuter;
976 + let innerPromise;
977 + let outerPromise;
978 + let OuterScheduler;
979 + let InnerScheduler;
980 + let innerDiv;
981 +
982 + beforeEach(async () => {
983 + document.body.innerHTML = '';
984 + jest.resetModuleRegistry();
985 + let OuterReactDOM;
986 + let InnerReactDOM;
987 + jest.isolateModules(() => {
988 + OuterReactDOM = require('react-dom');
989 + OuterScheduler = require('scheduler');
990 + });
991 + jest.isolateModules(() => {
992 + InnerReactDOM = require('react-dom');
993 + InnerScheduler = require('scheduler');
994 + });
995 +
996 + expect(OuterReactDOM).not.toBe(InnerReactDOM);
997 + expect(OuterScheduler).not.toBe(InnerScheduler);
998 +
999 + const outerContainer = document.createElement('div');
1000 + const innerContainer = document.createElement('div');
1001 +
1002 + let suspendOuter = false;
1003 + outerPromise = new Promise(res => {
1004 + resolveOuter = () => {
1005 + suspendOuter = false;
1006 + res();
1007 + };
1008 + });
1009 +
1010 + function Outer() {
1011 + if (suspendOuter) {
1012 + OuterScheduler.unstable_yieldValue('Suspend Outer');
1013 + throw outerPromise;
1014 + }
1015 + OuterScheduler.unstable_yieldValue('Outer');
1016 + const innerRoot = outerContainer.querySelector('#inner-root');
1017 + return (
1018 + <div
1019 + id="inner-root"
1020 + onMouseEnter={() => {
1021 + Scheduler.unstable_yieldValue('Outer Mouse Enter');
1022 + }}
1023 + dangerouslySetInnerHTML={{
1024 + __html: innerRoot ? innerRoot.innerHTML : '',
1025 + }}
1026 + />
1027 + );
1028 + }
1029 + const OuterApp = () => {
1030 + return (
1031 + <Suspense fallback={<div>Loading</div>}>
1032 + <Outer />
1033 + </Suspense>
1034 + );
1035 + };
1036 +
1037 + let suspendInner = false;
1038 + innerPromise = new Promise(res => {
1039 + resolveInner = () => {
1040 + suspendInner = false;
1041 + res();
1042 + };
1043 + });
1044 + function Inner() {
1045 + if (suspendInner) {
1046 + InnerScheduler.unstable_yieldValue('Suspend Inner');
1047 + throw innerPromise;
1048 + }
1049 + InnerScheduler.unstable_yieldValue('Inner');
1050 + return (
1051 + <div
1052 + id="inner"
1053 + onMouseEnter={() => {
1054 + Scheduler.unstable_yieldValue('Inner Mouse Enter');
1055 + }}
1056 + />
1057 + );
1058 + }
1059 + const InnerApp = () => {
1060 + return (
1061 + <Suspense fallback={<div>Loading</div>}>
1062 + <Inner />
1063 + </Suspense>
1064 + );
1065 + };
1066 +
1067 + document.body.appendChild(outerContainer);
1068 + const outerHTML = ReactDOMServer.renderToString(<OuterApp />);
1069 + outerContainer.innerHTML = outerHTML;
1070 +
1071 + const innerWrapper = document.querySelector('#inner-root');
1072 + innerWrapper.appendChild(innerContainer);
1073 + const innerHTML = ReactDOMServer.renderToString(<InnerApp />);
1074 + innerContainer.innerHTML = innerHTML;
1075 +
1076 + expect(OuterScheduler).toHaveYielded(['Outer']);
1077 + expect(InnerScheduler).toHaveYielded(['Inner']);
1078 +
1079 + suspendOuter = true;
1080 + suspendInner = true;
1081 +
1082 + OuterReactDOM.hydrateRoot(outerContainer, <OuterApp />);
1083 + InnerReactDOM.hydrateRoot(innerContainer, <InnerApp />);
1084 +
1085 + expect(OuterScheduler).toFlushAndYield(['Suspend Outer']);
1086 + expect(InnerScheduler).toFlushAndYield(['Suspend Inner']);
1087 +
1088 + innerDiv = document.querySelector('#inner');
1089 +
1090 + dispatchClickEvent(innerDiv);
1091 +
1092 + await act(async () => {
1093 + jest.runAllTimers();
1094 + Scheduler.unstable_flushAllWithoutAsserting();
1095 + OuterScheduler.unstable_flushAllWithoutAsserting();
1096 + InnerScheduler.unstable_flushAllWithoutAsserting();
1097 + });
1098 +
1099 + expect(OuterScheduler).toHaveYielded(['Suspend Outer']);
1100 + if (
1101 + gate(
1102 + flags =>
1103 + flags.enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
1104 + )
1105 + ) {
1106 + // InnerApp doesn't see the event because OuterApp calls stopPropagation in
1107 + // capture phase since the event is blocked on suspended component
1108 + expect(InnerScheduler).toHaveYielded([]);
1109 + } else {
1110 + // no stopPropagation
1111 + expect(InnerScheduler).toHaveYielded(['Suspend Inner']);
1112 + }
1113 +
1114 + expect(Scheduler).toHaveYielded([]);
1115 + });
1116 + afterEach(async () => {
1117 + document.body.innerHTML = '';
1118 + });
1119 +
1120 + // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1121 + it('Inner hydrates first then Outer', async () => {
1122 + dispatchMouseHoverEvent(innerDiv);
1123 +
1124 + await act(async () => {
1125 + resolveInner();
1126 + await innerPromise;
1127 + jest.runAllTimers();
1128 + Scheduler.unstable_flushAllWithoutAsserting();
1129 + OuterScheduler.unstable_flushAllWithoutAsserting();
1130 + InnerScheduler.unstable_flushAllWithoutAsserting();
1131 + });
1132 +
1133 + expect(OuterScheduler).toHaveYielded(['Suspend Outer']);
1134 + // Inner App renders because it is unblocked
1135 + expect(InnerScheduler).toHaveYielded(['Inner']);
1136 + // No event is replayed yet
1137 + expect(Scheduler).toHaveYielded([]);
1138 +
1139 + dispatchMouseHoverEvent(innerDiv);
1140 + expect(OuterScheduler).toHaveYielded([]);
1141 + expect(InnerScheduler).toHaveYielded([]);
1142 + // No event is replayed yet
1143 + expect(Scheduler).toHaveYielded([]);
1144 +
1145 + await act(async () => {
1146 + resolveOuter();
1147 + await outerPromise;
1148 + jest.runAllTimers();
1149 + Scheduler.unstable_flushAllWithoutAsserting();
1150 + OuterScheduler.unstable_flushAllWithoutAsserting();
1151 + InnerScheduler.unstable_flushAllWithoutAsserting();
1152 + });
1153 +
1154 + // Nothing happens to inner app yet.
1155 + // Its blocked on the outer app replaying the event
1156 + expect(InnerScheduler).toHaveYielded([]);
1157 + // Outer hydrates and schedules Replay
1158 + expect(OuterScheduler).toHaveYielded(['Outer']);
1159 + // No event is replayed yet
1160 + expect(Scheduler).toHaveYielded([]);
1161 +
1162 + // fire scheduled Replay
1163 + await act(async () => {
1164 + jest.runAllTimers();
1165 + Scheduler.unstable_flushAllWithoutAsserting();
1166 + OuterScheduler.unstable_flushAllWithoutAsserting();
1167 + InnerScheduler.unstable_flushAllWithoutAsserting();
1168 + });
1169 +
1170 + // First Inner Mouse Enter fires then Outer Mouse Enter
1171 + expect(Scheduler).toHaveYielded([
1172 + 'Inner Mouse Enter',
1173 + 'Outer Mouse Enter',
1174 + ]);
1175 + });
1176 +
1177 + // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1178 + it('Outer hydrates first then Inner', async () => {
1179 + dispatchMouseHoverEvent(innerDiv);
1180 +
1181 + await act(async () => {
1182 + resolveOuter();
1183 + await outerPromise;
1184 + Scheduler.unstable_flushAllWithoutAsserting();
1185 + OuterScheduler.unstable_flushAllWithoutAsserting();
1186 + InnerScheduler.unstable_flushAllWithoutAsserting();
1187 + });
1188 +
1189 + // Outer resolves and scheduled replay
1190 + expect(OuterScheduler).toHaveYielded(['Outer']);
1191 + // Inner App is still blocked
1192 + expect(InnerScheduler).toHaveYielded([]);
1193 +
1194 + // Replay outer event
1195 + await act(async () => {
1196 + Scheduler.unstable_flushAllWithoutAsserting();
1197 + OuterScheduler.unstable_flushAllWithoutAsserting();
1198 + InnerScheduler.unstable_flushAllWithoutAsserting();
1199 + });
1200 +
1201 + // Inner is still blocked so when Outer replays the event in capture phase
1202 + // inner ends up caling stopPropagation
1203 + expect(Scheduler).toHaveYielded([]);
1204 + expect(OuterScheduler).toHaveYielded([]);
1205 + expect(InnerScheduler).toHaveYielded(['Suspend Inner']);
1206 +
1207 + dispatchMouseHoverEvent(innerDiv);
1208 + expect(OuterScheduler).toHaveYielded([]);
1209 + expect(InnerScheduler).toHaveYielded([]);
1210 + expect(Scheduler).toHaveYielded([]);
1211 +
1212 + await act(async () => {
1213 + resolveInner();
1214 + await innerPromise;
1215 + Scheduler.unstable_flushAllWithoutAsserting();
1216 + OuterScheduler.unstable_flushAllWithoutAsserting();
1217 + InnerScheduler.unstable_flushAllWithoutAsserting();
1218 + });
1219 +
1220 + // Inner hydrates
1221 + expect(InnerScheduler).toHaveYielded(['Inner']);
1222 + // Outer was hydrated earlier
1223 + expect(OuterScheduler).toHaveYielded([]);
1224 +
1225 + await act(async () => {
1226 + Scheduler.unstable_flushAllWithoutAsserting();
1227 + OuterScheduler.unstable_flushAllWithoutAsserting();
1228 + InnerScheduler.unstable_flushAllWithoutAsserting();
1229 + });
1230 +
1231 + // First Inner Mouse Enter fires then Outer Mouse Enter
1232 + expect(Scheduler).toHaveYielded([
1233 + 'Inner Mouse Enter',
1234 + 'Outer Mouse Enter',
1235 + ]);
1236 + });
1237 + });
1238 +
1239 + // @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1240 + it('replays event with null target when tree is dismounted', async () => {
1241 + let suspend = false;
1242 + let resolve;
1243 + const promise = new Promise(resolvePromise => {
1244 + resolve = () => {
1245 + suspend = false;
1246 + resolvePromise();
1247 + };
1248 + });
1249 +
1250 + function Child() {
1251 + if (suspend) {
1252 + throw promise;
1253 + }
1254 + Scheduler.unstable_yieldValue('Child');
1255 + return (
1256 + <div
1257 + onMouseOver={() => {
1258 + Scheduler.unstable_yieldValue('on mouse over');
1259 + }}>
1260 + Child
1261 + </div>
1262 + );
1263 + }
1264 +
1265 + function App() {
1266 + return (
1267 + <Suspense>
1268 + <Child />
1269 + </Suspense>
1270 + );
1271 + }
1272 +
1273 + const finalHTML = ReactDOMServer.renderToString(<App />);
1274 + expect(Scheduler).toHaveYielded(['Child']);
1275 +
1276 + const container = document.createElement('div');
1277 +
1278 + document.body.appendChild(container);
1279 + container.innerHTML = finalHTML;
1280 + suspend = true;
1281 +
1282 + ReactDOM.hydrateRoot(container, <App />);
1283 +
1284 + const childDiv = container.firstElementChild;
1285 + dispatchMouseHoverEvent(childDiv);
1286 +
1287 + // Not hydrated so event is saved for replay and stopPropagation is called
1288 + expect(Scheduler).toHaveYielded([]);
1289 +
1290 + resolve();
1291 + Scheduler.unstable_flushNumberOfYields(1);
1292 + expect(Scheduler).toHaveYielded(['Child']);
1293 +
1294 + Scheduler.unstable_scheduleCallback(
1295 + Scheduler.unstable_ImmediatePriority,
1296 + () => {
1297 + container.removeChild(childDiv);
1298 +
1299 + const container2 = document.createElement('div');
1300 + container2.addEventListener('mouseover', () => {
1301 + Scheduler.unstable_yieldValue('container2 mouse over');
1302 + });
1303 + container2.appendChild(childDiv);
1304 + },
1305 + );
1306 + Scheduler.unstable_flushAllWithoutAsserting();
1307 +
1308 + // Even though the tree is remove the event is still dispatched with native event handler
1309 + // on the container firing.
1310 + expect(Scheduler).toHaveYielded(['container2 mouse over']);
1311 +
1312 document.body.removeChild(container);
1313 });
1314
packages/react-dom/src/events/ReactDOMEventListener.js
+17 -45
@@ -267,31 +267,6 @@ function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEve
267 targetContainer: EventTarget,
268 nativeEvent: AnyNativeEvent,
269 ) {
270 - // TODO: replaying capture phase events is currently broken
271 - // because we used to do it during top-level native bubble handlers
272 - // but now we use different bubble and capture handlers.
273 - // In eager mode, we attach capture listeners early, so we need
274 - // to filter them out until we fix the logic to handle them correctly.
275 - const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
276 -
277 - if (
278 - allowReplay &&
279 - hasQueuedDiscreteEvents() &&
280 - isDiscreteEventThatRequiresHydration(domEventName)
281 - ) {
282 - // If we already have a queue of discrete events, and this is another discrete
283 - // event, then we can't dispatch it regardless of its target, since they
284 - // need to dispatch in order.
285 - queueDiscreteEvent(
286 - null, // Flags that we're not actually blocked on anything as far as we know.
287 - domEventName,
288 - eventSystemFlags,
289 - targetContainer,
290 - nativeEvent,
291 - );
292 - return;
293 - }
294 -
270 let blockedOn = findInstanceBlockingEvent(
271 domEventName,
272 eventSystemFlags,
@@ -306,28 +281,25 @@ function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEve
281 return_targetInst,
282 targetContainer,
283 );
309 - if (allowReplay) {
310 - clearIfContinuousEvent(domEventName, nativeEvent);
311 - }
284 + clearIfContinuousEvent(domEventName, nativeEvent);
285 return;
286 }
287
315 - if (allowReplay) {
316 - if (
317 - queueIfContinuousEvent(
318 - blockedOn,
319 - domEventName,
320 - eventSystemFlags,
321 - targetContainer,
322 - nativeEvent,
323 - )
324 - ) {
325 - return;
326 - }
327 - // We need to clear only if we didn't queue because
328 - // queueing is accumulative.
329 - clearIfContinuousEvent(domEventName, nativeEvent);
288 + if (
289 + queueIfContinuousEvent(
290 + blockedOn,
291 + domEventName,
292 + eventSystemFlags,
293 + targetContainer,
294 + nativeEvent,
295 + )
296 + ) {
297 + nativeEvent.stopPropagation();
298 + return;
299 }
300 + // We need to clear only if we didn't queue because
301 + // queueing is accumulative.
302 + clearIfContinuousEvent(domEventName, nativeEvent);
303
304 if (
305 eventSystemFlags & IS_CAPTURE_PHASE &&
@@ -358,10 +330,10 @@ function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEve
330 }
331 blockedOn = nextBlockedOn;
332 }
361 - if (blockedOn) {
333 + if (blockedOn !== null) {
334 nativeEvent.stopPropagation();
363 - return;
335 }
336 + return;
337 }
338
339 // This is not replayable so we'll invoke it but without a target,
packages/react-dom/src/events/ReactDOMEventReplaying.js
+22 -9
@@ -472,15 +472,26 @@ function attemptReplayContinuousQueuedEvent(
472 queuedEvent.nativeEvent,
473 );
474 if (nextBlockedOn === null) {
475 - setReplayingEvent(queuedEvent.nativeEvent);
476 - dispatchEventForPluginEventSystem(
477 - queuedEvent.domEventName,
478 - queuedEvent.eventSystemFlags,
479 - queuedEvent.nativeEvent,
480 - return_targetInst,
481 - targetContainer,
482 - );
483 - resetReplayingEvent();
475 + if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
476 + const nativeEvent = queuedEvent.nativeEvent;
477 + const nativeEventClone = new nativeEvent.constructor(
478 + nativeEvent.type,
479 + (nativeEvent: any),
480 + );
481 + setReplayingEvent(nativeEventClone);
482 + nativeEvent.target.dispatchEvent(nativeEventClone);
483 + resetReplayingEvent();
484 + } else {
485 + setReplayingEvent(queuedEvent.nativeEvent);
486 + dispatchEventForPluginEventSystem(
487 + queuedEvent.domEventName,
488 + queuedEvent.eventSystemFlags,
489 + queuedEvent.nativeEvent,
490 + return_targetInst,
491 + targetContainer,
492 + );
493 + resetReplayingEvent();
494 + }
495 } else {
496 // We're still blocked. Try again later.
497 const fiber = getInstanceFromNode(nextBlockedOn);
@@ -532,6 +543,8 @@ function replayUnblockedEvents() {
543 nextDiscreteEvent.nativeEvent,
544 );
545 if (nextBlockedOn === null) {
546 + // This whole function is in !enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
547 + // so we don't need the new replay behavior code branch.
548 setReplayingEvent(nextDiscreteEvent.nativeEvent);
549 dispatchEventForPluginEventSystem(
550 nextDiscreteEvent.domEventName,