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
salazarm committed
Oct 6, 2021 at 08:31 UTC
6ecad79ccf2c683cb21ac40dee5678358c7b6208
3 files changed
+76
-18
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+63
@@ -1130,4 +1130,67 @@ describe('ReactDOMServerSelectiveHydration', () => {
1130
1131
document.body.removeChild(container);
1132
});
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
+ });
1196
});
packages/react-dom/src/events/ReactDOMEventListener.js
+10
-10
@@ -11,18 +11,14 @@ 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 {
15
- enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
16
- enableSelectiveHydration,
17
-} from 'shared/ReactFeatureFlags';
18
-import {
19
- isReplayableDiscreteEvent,
16
+ isDiscreteEventThatRequiresHydration,
17
queueDiscreteEvent,
18
hasQueuedDiscreteEvents,
19
clearIfContinuousEvent,
20
queueIfContinuousEvent,
21
attemptSynchronousHydration,
25
- isCapturePhaseSynchronouslyHydratableEvent,
22
} from './ReactDOMEventReplaying';
23
import {
24
getNearestMountedFiber,
@@ -169,7 +165,7 @@ export function dispatchEvent(
165
if (
166
allowReplay &&
167
hasQueuedDiscreteEvents() &&
172
- isReplayableDiscreteEvent(domEventName)
168
+ isDiscreteEventThatRequiresHydration(domEventName)
169
) {
170
// If we already have a queue of discrete events, and this is another discrete
171
// event, then we can't dispatch it regardless of its target, since they
@@ -202,7 +198,7 @@ export function dispatchEvent(
198
if (allowReplay) {
199
if (
200
!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
205
- isReplayableDiscreteEvent(domEventName)
201
+ isDiscreteEventThatRequiresHydration(domEventName)
202
) {
203
// This this to be replayed later once the target is available.
204
queueDiscreteEvent(
@@ -232,8 +228,8 @@ export function dispatchEvent(
228
229
if (
230
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
235
- enableSelectiveHydration &&
236
- isCapturePhaseSynchronouslyHydratableEvent(domEventName)
231
+ eventSystemFlags & IS_CAPTURE_PHASE &&
232
+ isDiscreteEventThatRequiresHydration(domEventName)
233
) {
234
while (blockedOn !== null) {
235
const fiber = getInstanceFromNode(blockedOn);
@@ -251,6 +247,10 @@ export function dispatchEvent(
247
}
248
blockedOn = nextBlockedOn;
249
}
250
+ if (blockedOn) {
251
+ nativeEvent.stopPropagation();
252
+ return;
253
+ }
254
}
255
256
// This is not replayable so we'll invoke it but without a target,
packages/react-dom/src/events/ReactDOMEventReplaying.js
+3
-8
@@ -160,7 +160,9 @@ const discreteReplayableEvents: Array<DOMEventName> = [
160
'submit',
161
];
162
163
-export function isReplayableDiscreteEvent(eventType: DOMEventName): boolean {
163
+export function isDiscreteEventThatRequiresHydration(
164
+ eventType: DOMEventName,
165
+): boolean {
166
return discreteReplayableEvents.indexOf(eventType) > -1;
167
}
168
@@ -300,13 +302,6 @@ function accumulateOrCreateContinuousQueuedReplayableEvent(
302
return existingQueuedEvent;
303
}
304
303
-export function isCapturePhaseSynchronouslyHydratableEvent(
304
- eventName: DOMEventName,
305
-) {
306
- // TODO: maybe include more events
307
- return isReplayableDiscreteEvent(eventName);
308
-}
309
-
305
export function queueIfContinuousEvent(
306
blockedOn: null | Container | SuspenseInstance,
307
domEventName: DOMEventName,