Skip to content

Commit a88e173

Browse files
committed
lint, gating, codes.json
1 parent 6bf50cb commit a88e173

File tree

5 files changed

+154
-40
lines changed

5 files changed

+154
-40
lines changed

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,82 @@ describe('ReactDOMServerSelectiveHydration', () => {
12401240
});
12411241
});
12421242

1243+
// @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
1244+
it('replays event with null target when tree is dismounted', async () => {
1245+
let suspend = false;
1246+
let resolve;
1247+
const promise = new Promise(resolvePromise => {
1248+
resolve = () => {
1249+
suspend = false;
1250+
resolvePromise();
1251+
};
1252+
});
1253+
1254+
function Child() {
1255+
if (suspend) {
1256+
throw promise;
1257+
}
1258+
Scheduler.unstable_yieldValue('Child');
1259+
return (
1260+
<div
1261+
onMouseOver={() => {
1262+
Scheduler.unstable_yieldValue('on mouse over');
1263+
}}>
1264+
Child
1265+
</div>
1266+
);
1267+
}
1268+
1269+
function App() {
1270+
return (
1271+
<Suspense>
1272+
<Child />
1273+
</Suspense>
1274+
);
1275+
}
1276+
1277+
const finalHTML = ReactDOMServer.renderToString(<App />);
1278+
expect(Scheduler).toHaveYielded(['Child']);
1279+
1280+
const container = document.createElement('div');
1281+
1282+
document.body.appendChild(container);
1283+
container.innerHTML = finalHTML;
1284+
suspend = true;
1285+
1286+
ReactDOM.hydrateRoot(container, <App />);
1287+
1288+
const childDiv = container.firstElementChild;
1289+
dispatchMouseHoverEvent(childDiv);
1290+
1291+
// Not hydrated so event is saved for replay and stopPropagation is called
1292+
expect(Scheduler).toHaveYielded([]);
1293+
1294+
resolve();
1295+
Scheduler.unstable_flushNumberOfYields(1);
1296+
expect(Scheduler).toHaveYielded(['Child']);
1297+
1298+
Scheduler.unstable_scheduleCallback(
1299+
Scheduler.unstable_ImmediatePriority,
1300+
() => {
1301+
container.removeChild(childDiv);
1302+
1303+
const container2 = document.createElement('div');
1304+
container2.addEventListener('mouseover', () => {
1305+
Scheduler.unstable_yieldValue('container2 mouse over');
1306+
});
1307+
container2.appendChild(childDiv);
1308+
},
1309+
);
1310+
Scheduler.unstable_flushAllWithoutAsserting();
1311+
1312+
// Even though the tree is remove the event is still dispatched with native event handler
1313+
// on the container firing.
1314+
expect(Scheduler).toHaveYielded(['container2 mouse over']);
1315+
1316+
document.body.removeChild(container);
1317+
});
1318+
12431319
it('hydrates the last target path first for continuous events', async () => {
12441320
let suspend = false;
12451321
let resolve;

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

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
1313
import type {DOMEventName} from '../events/DOMEventNames';
1414
import {enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay} from 'shared/ReactFeatureFlags';
1515
import {
16+
nullTarget,
17+
NullTarget,
18+
isBlocked,
1619
isDiscreteEventThatRequiresHydration,
1720
queueDiscreteEvent,
1821
hasQueuedDiscreteEvents,
@@ -186,8 +189,8 @@ function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEve
186189
);
187190

188191
// We can dispatch the event now
189-
// Intentional double equals, either null or undefined
190-
if (blockedOn == null) {
192+
let blockedOnInst = isBlocked(blockedOn);
193+
if (!blockedOnInst) {
191194
clearIfContinuousEvent(domEventName, nativeEvent);
192195
dispatchEventForPluginEventSystem(
193196
domEventName,
@@ -221,9 +224,8 @@ function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEve
221224
eventSystemFlags & IS_CAPTURE_PHASE &&
222225
isDiscreteEventThatRequiresHydration(domEventName)
223226
) {
224-
// Intentionally not strict equal. Could be `null` or `undefined`
225-
while (blockedOn != null) {
226-
const fiber = getInstanceFromNode(blockedOn);
227+
while (blockedOnInst) {
228+
const fiber = getInstanceFromNode(blockedOnInst);
227229
if (fiber !== null) {
228230
attemptSynchronousHydration(fiber);
229231
}
@@ -237,8 +239,9 @@ function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEve
237239
break;
238240
}
239241
blockedOn = nextBlockedOn;
242+
blockedOnInst = isBlocked(blockedOn);
240243
}
241-
if (blockedOn) {
244+
if (blockedOnInst) {
242245
nativeEvent.stopPropagation();
243246
return;
244247
}
@@ -301,7 +304,8 @@ function dispatchEventOriginal(
301304
targetContainer,
302305
nativeEvent,
303306
);
304-
if (blockedOn == null) {
307+
const blockedOnInst = isBlocked(blockedOn);
308+
if (!blockedOnInst) {
305309
dispatchEventForPluginEventSystem(
306310
domEventName,
307311
eventSystemFlags,
@@ -359,13 +363,13 @@ function dispatchEventOriginal(
359363

360364
// Returns a SuspenseInstance or Container if it's blocked.
361365
// Returns null if not blocked and we should use closestInstance
362-
// Returns undefined if not blocked but we should dispatch without a targetInst
366+
// Returns nullTarget if not blocked but we should dispatch without a targetInst
363367
export function findInstanceBlockingEvent(
364368
domEventName: DOMEventName,
365369
eventSystemFlags: EventSystemFlags,
366370
targetContainer: EventTarget,
367371
nativeEvent: AnyNativeEvent,
368-
): typeof undefined | null | Container | SuspenseInstance {
372+
): NullTarget | null | Container | SuspenseInstance {
369373
// TODO: Warn if _enabled is false.
370374

371375
const nativeEventTarget = getEventTarget(nativeEvent);
@@ -380,31 +384,30 @@ export function findInstanceBlockingEvent(
380384
if (instance !== null) {
381385
// Queue the event to be replayed later. Abort dispatching since we
382386
// don't want this event dispatched twice through the event system.
383-
// TODO: If this is the first discrete event in the queue. Schedule an increased
384-
// priority for this boundary.
385387
return instance;
386388
}
387389
// This shouldn't happen, something went wrong but to avoid blocking
388390
// the whole system, dispatch the event without a target.
389391
// TODO: Warn.
390-
return undefined;
392+
return nullTarget;
391393
} else if (tag === HostRoot) {
392394
const root: FiberRoot = nearestMounted.stateNode;
393395
if (root.isDehydrated) {
394396
// If this happens during a replay something went wrong and it might block
395397
// the whole system.
396398
return getContainerFromFiber(nearestMounted);
397399
}
400+
return nullTarget;
398401
} else if (nearestMounted !== targetInst) {
399402
// If we get an event (ex: img onload) before committing that
400403
// component's mount, ignore it for now (that is, treat it as if it was an
401404
// event on a non-React tree). We might also consider queueing events and
402405
// dispatching them after the mount.
403-
return undefined;
406+
return nullTarget;
404407
}
405408
} else {
406409
// This tree has been unmounted already. Dispatch without a target.
407-
return undefined;
410+
return nullTarget;
408411
}
409412
}
410413
// We're not blocked on anything.

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

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,18 @@ type PointerEvent = Event & {
9090
...
9191
};
9292

93+
declare export class NullTarget {}
94+
export const nullTarget: NullTarget = ({}: any);
95+
export function isBlocked(
96+
blockedOn: NullTarget | null | Container | SuspenseInstance,
97+
): Container | SuspenseInstance | void {
98+
if (blockedOn !== nullTarget && blockedOn !== null) {
99+
return ((blockedOn: any): Container | SuspenseInstance);
100+
}
101+
}
102+
93103
type QueuedReplayableEvent = {|
94-
blockedOn: typeof undefined | null | Container | SuspenseInstance,
104+
blockedOn: NullTarget | null | Container | SuspenseInstance,
95105
domEventName: DOMEventName,
96106
eventSystemFlags: EventSystemFlags,
97107
nativeEvent: AnyNativeEvent,
@@ -116,7 +126,7 @@ const queuedPointerCaptures: Map<number, QueuedReplayableEvent> = new Map();
116126
// We could consider replaying selectionchange and touchmoves too.
117127

118128
type QueuedHydrationTarget = {|
119-
blockedOn: typeof undefined | null | Container | SuspenseInstance,
129+
blockedOn: NullTarget | null | Container | SuspenseInstance,
120130
target: Node,
121131
priority: EventPriority,
122132
|};
@@ -168,7 +178,7 @@ export function isDiscreteEventThatRequiresHydration(
168178
}
169179

170180
function createQueuedReplayableEvent(
171-
blockedOn: typeof undefined | null | Container | SuspenseInstance,
181+
blockedOn: NullTarget | null | Container | SuspenseInstance,
172182
domEventName: DOMEventName,
173183
eventSystemFlags: EventSystemFlags,
174184
targetContainer: EventTarget,
@@ -184,7 +194,7 @@ function createQueuedReplayableEvent(
184194
}
185195

186196
export function queueDiscreteEvent(
187-
blockedOn: typeof undefined | null | Container | SuspenseInstance,
197+
blockedOn: NullTarget | null | Container | SuspenseInstance,
188198
domEventName: DOMEventName,
189199
eventSystemFlags: EventSystemFlags,
190200
targetContainer: EventTarget,
@@ -205,13 +215,15 @@ export function queueDiscreteEvent(
205215
if (queuedDiscreteEvents.length === 1) {
206216
// If this was the first discrete event, we might be able to
207217
// synchronously unblock it so that preventDefault still works.
208-
while (queuedEvent.blockedOn != null) {
209-
const fiber = getInstanceFromNode(queuedEvent.blockedOn);
218+
let blockedOnInst;
219+
while ((blockedOnInst = isBlocked(queuedEvent.blockedOn))) {
220+
const fiber = getInstanceFromNode(blockedOnInst);
210221
if (fiber === null) {
211222
break;
212223
}
213224
attemptSynchronousHydration(fiber);
214-
if (queuedEvent.blockedOn == null) {
225+
blockedOnInst = isBlocked(queuedEvent.blockedOn);
226+
if (!blockedOnInst) {
215227
// We got unblocked by hydration. Let's try again.
216228
replayUnblockedEvents();
217229
// If we're reblocked, on an inner boundary, we might need
@@ -262,7 +274,7 @@ export function clearIfContinuousEvent(
262274

263275
function accumulateOrCreateContinuousQueuedReplayableEvent(
264276
existingQueuedEvent: null | QueuedReplayableEvent,
265-
blockedOn: typeof undefined | null | Container | SuspenseInstance,
277+
blockedOn: NullTarget | null | Container | SuspenseInstance,
266278
domEventName: DOMEventName,
267279
eventSystemFlags: EventSystemFlags,
268280
targetContainer: EventTarget,
@@ -279,8 +291,9 @@ function accumulateOrCreateContinuousQueuedReplayableEvent(
279291
targetContainer,
280292
nativeEvent,
281293
);
282-
if (blockedOn != null) {
283-
const fiber = getInstanceFromNode(blockedOn);
294+
const blockedOnInst = isBlocked(blockedOn);
295+
if (blockedOnInst) {
296+
const fiber = getInstanceFromNode(blockedOnInst);
284297
if (fiber !== null) {
285298
// Attempt to increase the priority of this target.
286299
attemptContinuousHydration(fiber);
@@ -304,7 +317,7 @@ function accumulateOrCreateContinuousQueuedReplayableEvent(
304317
}
305318

306319
export function queueIfContinuousEvent(
307-
blockedOn: typeof undefined | null | Container | SuspenseInstance,
320+
blockedOn: NullTarget | null | Container | SuspenseInstance,
308321
domEventName: DOMEventName,
309322
eventSystemFlags: EventSystemFlags,
310323
targetContainer: EventTarget,
@@ -386,7 +399,6 @@ export function queueIfContinuousEvent(
386399
return false;
387400
}
388401

389-
// Check if this target is unblocked. Returns true if it's unblocked.
390402
function attemptExplicitHydrationTarget(
391403
queuedTarget: QueuedHydrationTarget,
392404
): void {
@@ -454,7 +466,8 @@ export function queueExplicitHydrationTarget(target: Node): void {
454466
function attemptReplayContinuousQueuedEvent(
455467
queuedEvent: QueuedReplayableEvent,
456468
): boolean {
457-
if (queuedEvent.blockedOn != null) {
469+
const blockedOnInst = isBlocked(queuedEvent.blockedOn);
470+
if (blockedOnInst) {
458471
return false;
459472
}
460473
const targetContainers = queuedEvent.targetContainers;
@@ -466,16 +479,17 @@ function attemptReplayContinuousQueuedEvent(
466479
targetContainer,
467480
queuedEvent.nativeEvent,
468481
);
469-
if (nextBlockedOn != null) {
482+
const nextBlockedOnInst = isBlocked(nextBlockedOn);
483+
if (nextBlockedOnInst) {
470484
// We're still blocked. Try again later.
471-
const fiber = getInstanceFromNode(nextBlockedOn);
485+
const fiber = getInstanceFromNode(nextBlockedOnInst);
472486
if (fiber !== null) {
473487
attemptContinuousHydration(fiber);
474488
}
475489
queuedEvent.blockedOn = nextBlockedOn;
476490
return false;
477491
}
478-
replayEvent(queuedEvent, targetContainer);
492+
replayEvent(queuedEvent, targetContainer, nextBlockedOn);
479493
// This target container was successfully dispatched. Try the next.
480494
targetContainers.shift();
481495
}
@@ -498,11 +512,13 @@ function replayUnblockedEvents() {
498512
// First replay discrete events.
499513
while (queuedDiscreteEvents.length > 0) {
500514
const nextDiscreteEvent = queuedDiscreteEvents[0];
501-
if (nextDiscreteEvent.blockedOn != null) {
515+
const blockedOn = nextDiscreteEvent.blockedOn;
516+
const blockedOnInst = isBlocked(blockedOn);
517+
if (blockedOnInst) {
502518
// We're still blocked.
503519
// Increase the priority of this boundary to unblock
504520
// the next discrete event.
505-
const fiber = getInstanceFromNode(nextDiscreteEvent.blockedOn);
521+
const fiber = getInstanceFromNode(blockedOnInst);
506522
if (fiber !== null) {
507523
attemptDiscreteHydration(fiber);
508524
}
@@ -517,16 +533,20 @@ function replayUnblockedEvents() {
517533
targetContainer,
518534
nextDiscreteEvent.nativeEvent,
519535
);
520-
if (nextBlockedOn != null) {
536+
const nextBlockedOnInst = isBlocked(nextBlockedOn);
537+
if (nextBlockedOnInst) {
521538
// We're still blocked. Try again later.
522-
nextDiscreteEvent.blockedOn = nextBlockedOn;
539+
nextDiscreteEvent.blockedOn = nextBlockedOnInst;
523540
break;
524541
}
525-
replayEvent(nextDiscreteEvent, targetContainer);
542+
replayEvent(nextDiscreteEvent, targetContainer, nextBlockedOn);
526543
// This target container was successfully dispatched. Try the next.
527544
targetContainers.shift();
528545
}
529-
if (nextDiscreteEvent.blockedOn == null) {
546+
if (
547+
nextDiscreteEvent.blockedOn === null ||
548+
nextDiscreteEvent.blockedOn === nullTarget
549+
) {
530550
// We've successfully replayed the first event. Let's try the next one.
531551
queuedDiscreteEvents.shift();
532552
}
@@ -603,12 +623,13 @@ export function retryIfBlockedOn(
603623

604624
while (queuedExplicitHydrationTargets.length > 0) {
605625
const nextExplicitTarget = queuedExplicitHydrationTargets[0];
606-
if (nextExplicitTarget.blockedOn != null) {
626+
const blockedOnInst = isBlocked(nextExplicitTarget.blockedOn);
627+
if (blockedOnInst) {
607628
// We're still blocked.
608629
break;
609630
} else {
610631
attemptExplicitHydrationTarget(nextExplicitTarget);
611-
if (nextExplicitTarget.blockedOn == null) {
632+
if (!isBlocked(nextExplicitTarget.blockedOn)) {
612633
// We're unblocked.
613634
queuedExplicitHydrationTargets.shift();
614635
}
@@ -619,6 +640,7 @@ export function retryIfBlockedOn(
619640
function replayEvent(
620641
queuedEvent: QueuedReplayableEvent,
621642
targetContainer: EventTarget,
643+
targetInst: null | NullTarget | Container | SuspenseInstance,
622644
) {
623645
const event = queuedEvent.nativeEvent;
624646
if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
@@ -632,7 +654,9 @@ function replayEvent(
632654
queuedEvent.domEventName,
633655
queuedEvent.eventSystemFlags,
634656
event,
635-
getClosestInstanceFromNode(getEventTarget(queuedEvent.nativeEvent)),
657+
targetInst === nullTarget
658+
? null
659+
: getClosestInstanceFromNode(getEventTarget(queuedEvent.nativeEvent)),
636660
targetContainer,
637661
);
638662
});

0 commit comments

Comments
 (0)