Skip to content

Commit 6ecad79

Browse files
authored
Test that discrete events that aren't hydratable do not propagate (#22502)
* test that discrete events that arent hydratable do not propagate * lint * feedback * feedback * lint * better test * nits * lint
1 parent 579c008 commit 6ecad79

File tree

3 files changed

+76
-18
lines changed

3 files changed

+76
-18
lines changed

packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,4 +1130,67 @@ describe('ReactDOMServerSelectiveHydration', () => {
11301130

11311131
document.body.removeChild(container);
11321132
});
1133+
1134+
// @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1135+
it('does not propagate discrete event if it cannot be synchronously hydrated', async () => {
1136+
let triggeredParent = false;
1137+
let triggeredChild = false;
1138+
let suspend = false;
1139+
const promise = new Promise(() => {});
1140+
function Child() {
1141+
if (suspend) {
1142+
throw promise;
1143+
}
1144+
Scheduler.unstable_yieldValue('Child');
1145+
return (
1146+
<span
1147+
onClickCapture={e => {
1148+
e.stopPropagation();
1149+
triggeredChild = true;
1150+
}}>
1151+
Click me
1152+
</span>
1153+
);
1154+
}
1155+
function App() {
1156+
const onClick = () => {
1157+
triggeredParent = true;
1158+
};
1159+
Scheduler.unstable_yieldValue('App');
1160+
return (
1161+
<div
1162+
ref={n => {
1163+
if (n) n.onclick = onClick;
1164+
}}
1165+
onClick={onClick}>
1166+
<Suspense fallback={null}>
1167+
<Child />
1168+
</Suspense>
1169+
</div>
1170+
);
1171+
}
1172+
const finalHTML = ReactDOMServer.renderToString(<App />);
1173+
1174+
expect(Scheduler).toHaveYielded(['App', 'Child']);
1175+
1176+
const container = document.createElement('div');
1177+
document.body.appendChild(container);
1178+
container.innerHTML = finalHTML;
1179+
1180+
suspend = true;
1181+
1182+
ReactDOM.hydrateRoot(container, <App />);
1183+
// Nothing has been hydrated so far.
1184+
expect(Scheduler).toHaveYielded([]);
1185+
1186+
const span = container.getElementsByTagName('span')[0];
1187+
dispatchClickEvent(span);
1188+
1189+
expect(Scheduler).toHaveYielded(['App']);
1190+
1191+
dispatchClickEvent(span);
1192+
1193+
expect(triggeredParent).toBe(false);
1194+
expect(triggeredChild).toBe(false);
1195+
});
11331196
});

packages/react-dom/src/events/ReactDOMEventListener.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,14 @@ import type {AnyNativeEvent} from '../events/PluginModuleType';
1111
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
1212
import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
1313
import type {DOMEventName} from '../events/DOMEventNames';
14+
import {enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay} from 'shared/ReactFeatureFlags';
1415
import {
15-
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
16-
enableSelectiveHydration,
17-
} from 'shared/ReactFeatureFlags';
18-
import {
19-
isReplayableDiscreteEvent,
16+
isDiscreteEventThatRequiresHydration,
2017
queueDiscreteEvent,
2118
hasQueuedDiscreteEvents,
2219
clearIfContinuousEvent,
2320
queueIfContinuousEvent,
2421
attemptSynchronousHydration,
25-
isCapturePhaseSynchronouslyHydratableEvent,
2622
} from './ReactDOMEventReplaying';
2723
import {
2824
getNearestMountedFiber,
@@ -169,7 +165,7 @@ export function dispatchEvent(
169165
if (
170166
allowReplay &&
171167
hasQueuedDiscreteEvents() &&
172-
isReplayableDiscreteEvent(domEventName)
168+
isDiscreteEventThatRequiresHydration(domEventName)
173169
) {
174170
// If we already have a queue of discrete events, and this is another discrete
175171
// event, then we can't dispatch it regardless of its target, since they
@@ -202,7 +198,7 @@ export function dispatchEvent(
202198
if (allowReplay) {
203199
if (
204200
!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
205-
isReplayableDiscreteEvent(domEventName)
201+
isDiscreteEventThatRequiresHydration(domEventName)
206202
) {
207203
// This this to be replayed later once the target is available.
208204
queueDiscreteEvent(
@@ -232,8 +228,8 @@ export function dispatchEvent(
232228

233229
if (
234230
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
235-
enableSelectiveHydration &&
236-
isCapturePhaseSynchronouslyHydratableEvent(domEventName)
231+
eventSystemFlags & IS_CAPTURE_PHASE &&
232+
isDiscreteEventThatRequiresHydration(domEventName)
237233
) {
238234
while (blockedOn !== null) {
239235
const fiber = getInstanceFromNode(blockedOn);
@@ -251,6 +247,10 @@ export function dispatchEvent(
251247
}
252248
blockedOn = nextBlockedOn;
253249
}
250+
if (blockedOn) {
251+
nativeEvent.stopPropagation();
252+
return;
253+
}
254254
}
255255

256256
// This is not replayable so we'll invoke it but without a target,

packages/react-dom/src/events/ReactDOMEventReplaying.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ const discreteReplayableEvents: Array<DOMEventName> = [
160160
'submit',
161161
];
162162

163-
export function isReplayableDiscreteEvent(eventType: DOMEventName): boolean {
163+
export function isDiscreteEventThatRequiresHydration(
164+
eventType: DOMEventName,
165+
): boolean {
164166
return discreteReplayableEvents.indexOf(eventType) > -1;
165167
}
166168

@@ -300,13 +302,6 @@ function accumulateOrCreateContinuousQueuedReplayableEvent(
300302
return existingQueuedEvent;
301303
}
302304

303-
export function isCapturePhaseSynchronouslyHydratableEvent(
304-
eventName: DOMEventName,
305-
) {
306-
// TODO: maybe include more events
307-
return isReplayableDiscreteEvent(eventName);
308-
}
309-
310305
export function queueIfContinuousEvent(
311306
blockedOn: null | Container | SuspenseInstance,
312307
domEventName: DOMEventName,

0 commit comments

Comments
 (0)