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.