Skip to content
Closed

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/react-dom/src/events/DOMPluginEventSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
import {isReplayingEvent} from './replayedEvent';

type DispatchListener = {|
instance: null | Fiber,
Expand Down Expand Up @@ -557,7 +558,8 @@ export function dispatchEventForPluginEventSystem(
// for legacy FB support, where the expected behavior was to
// match React < 16 behavior of delegated clicks to the doc.
domEventName === 'click' &&
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 &&
!isReplayingEvent(nativeEvent)
) {
deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
return;
Expand Down
5 changes: 2 additions & 3 deletions packages/react-dom/src/events/EventSystemFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ export const IS_EVENT_HANDLE_NON_MANAGED_NODE = 1;
export const IS_NON_DELEGATED = 1 << 1;
export const IS_CAPTURE_PHASE = 1 << 2;
export const IS_PASSIVE = 1 << 3;
export const IS_REPLAYED = 1 << 4;
export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 5;
export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 4;

export const SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE =
IS_LEGACY_FB_SUPPORT_MODE | IS_REPLAYED | IS_CAPTURE_PHASE;
IS_LEGACY_FB_SUPPORT_MODE | IS_CAPTURE_PHASE;

// We do not want to defer if the event system has already been
// set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when
Expand Down
200 changes: 145 additions & 55 deletions packages/react-dom/src/events/ReactDOMEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import type {AnyNativeEvent} from '../events/PluginModuleType';
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
import type {DOMEventName} from '../events/DOMEventNames';
import type {NullTarget} from './ReactDOMEventReplaying';
import {enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay} from 'shared/ReactFeatureFlags';
import {
nullTarget,
isBlocked,
isDiscreteEventThatRequiresHydration,
queueDiscreteEvent,
hasQueuedDiscreteEvents,
Expand Down Expand Up @@ -155,13 +158,128 @@ export function dispatchEvent(
return;
}

if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
} else {
dispatchEventOriginal(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
}
}

function dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
) {
let blockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);

// We can dispatch the event now
let blockedOnInst = isBlocked(blockedOn);
if (!blockedOnInst) {
clearIfContinuousEvent(domEventName, nativeEvent);
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
blockedOn === null
? getClosestInstanceFromNode(getEventTarget(nativeEvent))
: null,
targetContainer,
);
return;
}

if (
queueIfContinuousEvent(
blockedOn,
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
)
) {
nativeEvent.stopPropagation();
return;
}
// We need to clear only if we didn't queue because
// queueing is accumulative.
clearIfContinuousEvent(domEventName, nativeEvent);

if (
eventSystemFlags & IS_CAPTURE_PHASE &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
while (blockedOnInst) {
const fiber = getInstanceFromNode(blockedOnInst);
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
const nextBlockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (nextBlockedOn === blockedOn) {
break;
}
blockedOn = nextBlockedOn;
blockedOnInst = isBlocked(blockedOn);
}
if (blockedOnInst) {
nativeEvent.stopPropagation();
return;
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
blockedOn === null
? getClosestInstanceFromNode(getEventTarget(nativeEvent))
: null,
targetContainer,
);
return;
}

// This is not replayable so we'll invoke it but without a target,
// in case the event system needs to trace it.
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}

function dispatchEventOriginal(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
) {
// TODO: replaying capture phase events is currently broken
// because we used to do it during top-level native bubble handlers
// but now we use different bubble and capture handlers.
// In eager mode, we attach capture listeners early, so we need
// to filter them out until we fix the logic to handle them correctly.
const allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;

if (
allowReplay &&
hasQueuedDiscreteEvents() &&
Expand All @@ -180,14 +298,23 @@ export function dispatchEvent(
return;
}

let blockedOn = attemptToDispatchEvent(
const blockedOn = findInstanceBlockingEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);

if (blockedOn === null) {
const blockedOnInst = isBlocked(blockedOn);
if (!blockedOnInst) {
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
blockedOn === null
? getClosestInstanceFromNode(getEventTarget(nativeEvent))
: null,
targetContainer,
);
// We successfully dispatched this event.
if (allowReplay) {
clearIfContinuousEvent(domEventName, nativeEvent);
Expand All @@ -196,10 +323,7 @@ export function dispatchEvent(
}

if (allowReplay) {
if (
!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
if (isDiscreteEventThatRequiresHydration(domEventName)) {
// This this to be replayed later once the target is available.
queueDiscreteEvent(
blockedOn,
Expand All @@ -226,33 +350,6 @@ export function dispatchEvent(
clearIfContinuousEvent(domEventName, nativeEvent);
}

if (
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
eventSystemFlags & IS_CAPTURE_PHASE &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
while (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn);
if (fiber !== null) {
attemptSynchronousHydration(fiber);
}
const nextBlockedOn = attemptToDispatchEvent(
domEventName,
eventSystemFlags,
targetContainer,
nativeEvent,
);
if (nextBlockedOn === blockedOn) {
break;
}
blockedOn = nextBlockedOn;
}
if (blockedOn) {
nativeEvent.stopPropagation();
return;
}
}

// This is not replayable so we'll invoke it but without a target,
// in case the event system needs to trace it.
dispatchEventForPluginEventSystem(
Expand All @@ -264,62 +361,55 @@ export function dispatchEvent(
);
}

// Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked.
export function attemptToDispatchEvent(
// Returns a SuspenseInstance or Container if it's blocked.
// Returns null if not blocked and we should use closestInstance
// Returns nullTarget if not blocked but we should dispatch without a targetInst
export function findInstanceBlockingEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
): null | Container | SuspenseInstance {
): NullTarget | null | Container | SuspenseInstance {
// TODO: Warn if _enabled is false.

const nativeEventTarget = getEventTarget(nativeEvent);
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
const targetInst = getClosestInstanceFromNode(nativeEventTarget);

if (targetInst !== null) {
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// This tree has been unmounted already. Dispatch without a target.
targetInst = null;
} else {
if (nearestMounted !== null) {
const tag = nearestMounted.tag;
if (tag === SuspenseComponent) {
const instance = getSuspenseInstanceFromFiber(nearestMounted);
if (instance !== null) {
// Queue the event to be replayed later. Abort dispatching since we
// don't want this event dispatched twice through the event system.
// TODO: If this is the first discrete event in the queue. Schedule an increased
// priority for this boundary.
return instance;
}
// This shouldn't happen, something went wrong but to avoid blocking
// the whole system, dispatch the event without a target.
// TODO: Warn.
targetInst = null;
return nullTarget;
} else if (tag === HostRoot) {
const root: FiberRoot = nearestMounted.stateNode;
if (root.isDehydrated) {
// If this happens during a replay something went wrong and it might block
// the whole system.
return getContainerFromFiber(nearestMounted);
}
targetInst = null;
return nullTarget;
} else if (nearestMounted !== targetInst) {
// If we get an event (ex: img onload) before committing that
// component's mount, ignore it for now (that is, treat it as if it was an
// event on a non-React tree). We might also consider queueing events and
// dispatching them after the mount.
targetInst = null;
return nullTarget;
}
} else {
// This tree has been unmounted already. Dispatch without a target.
return nullTarget;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't come up with a test case where we findInstanceBlockingEvent would return nullTarget. It doesn't seem possible that it would ever happen because it would mean that there is a React Instance defined on the target but then nearestMountedFiber returns null for this case. I added a test which attempted to simulate this by removing the target node from its react tree into a tree where there is no react root but that didn't seem to work.

getClosestInstanceFromNode(getEventTarget(queuedEvent.nativeEvent)) did return null for this case though.

Based off how hydration works it shouldn't be possible for the nearestMountedFiber to ever be anything other than a SuspenseInstance. Unless the node is manually moved in userland to a different tree thats already hydrated, I tried doing that but that also didn't trigger the nullTarget. I'm unsure what else to try to trigger it. Any ideas?

}
}
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
// We're not blocked on anything.
return null;
}
Expand Down
Loading