Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test that discrete events that aren't hydratable do not propagate #22502

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1130,4 +1130,67 @@ describe('ReactDOMServerSelectiveHydration', () => {

document.body.removeChild(container);
});

// @gate enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay
it('does not propagate discrete event if it cannot be synchronously hydrated', async () => {
let triggeredParent = false;
let triggeredChild = false;
let suspend = false;
const promise = new Promise(() => {});
function Child() {
if (suspend) {
throw promise;
}
Scheduler.unstable_yieldValue('Child');
return (
<span
onClickCapture={e => {
e.stopPropagation();
triggeredChild = true;
}}>
Click me
</span>
);
}
function App() {
const onClick = () => {
triggeredParent = true;
};
Scheduler.unstable_yieldValue('App');
return (
<div
ref={n => {
if (n) n.onclick = onClick;
}}
onClick={onClick}>
<Suspense fallback={null}>
<Child />
</Suspense>
</div>
);
}
const finalHTML = ReactDOMServer.renderToString(<App />);

expect(Scheduler).toHaveYielded(['App', 'Child']);

const container = document.createElement('div');
document.body.appendChild(container);
container.innerHTML = finalHTML;

suspend = true;

ReactDOM.hydrateRoot(container, <App />);
// Nothing has been hydrated so far.
expect(Scheduler).toHaveYielded([]);

const span = container.getElementsByTagName('span')[0];
dispatchClickEvent(span);

expect(Scheduler).toHaveYielded(['App']);

dispatchClickEvent(span);

expect(triggeredParent).toBe(false);
expect(triggeredChild).toBe(false);
});
});
20 changes: 10 additions & 10 deletions packages/react-dom/src/events/ReactDOMEventListener.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,14 @@ 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 {enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay} from 'shared/ReactFeatureFlags';
import {
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
enableSelectiveHydration,
} from 'shared/ReactFeatureFlags';
import {
isReplayableDiscreteEvent,
isDiscreteEventThatRequiresHydration,
queueDiscreteEvent,
hasQueuedDiscreteEvents,
clearIfContinuousEvent,
queueIfContinuousEvent,
attemptSynchronousHydration,
isCapturePhaseSynchronouslyHydratableEvent,
} from './ReactDOMEventReplaying';
import {
getNearestMountedFiber,
Expand Down Expand Up @@ -169,7 +165,7 @@ export function dispatchEvent(
if (
allowReplay &&
hasQueuedDiscreteEvents() &&
isReplayableDiscreteEvent(domEventName)
isDiscreteEventThatRequiresHydration(domEventName)
) {
// If we already have a queue of discrete events, and this is another discrete
// event, then we can't dispatch it regardless of its target, since they
Expand Down Expand Up @@ -202,7 +198,7 @@ export function dispatchEvent(
if (allowReplay) {
if (
!enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
isReplayableDiscreteEvent(domEventName)
isDiscreteEventThatRequiresHydration(domEventName)
) {
// This this to be replayed later once the target is available.
queueDiscreteEvent(
Expand Down Expand Up @@ -232,8 +228,8 @@ export function dispatchEvent(

if (
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay &&
enableSelectiveHydration &&
isCapturePhaseSynchronouslyHydratableEvent(domEventName)
eventSystemFlags & IS_CAPTURE_PHASE &&
isDiscreteEventThatRequiresHydration(domEventName)
) {
while (blockedOn !== null) {
const fiber = getInstanceFromNode(blockedOn);
Expand All @@ -251,6 +247,10 @@ export function dispatchEvent(
}
blockedOn = nextBlockedOn;
}
if (blockedOn) {
nativeEvent.stopPropagation();
return;
}
}

// This is not replayable so we'll invoke it but without a target,
Expand Down
11 changes: 3 additions & 8 deletions packages/react-dom/src/events/ReactDOMEventReplaying.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ const discreteReplayableEvents: Array<DOMEventName> = [
'submit',
];

export function isReplayableDiscreteEvent(eventType: DOMEventName): boolean {
export function isDiscreteEventThatRequiresHydration(
eventType: DOMEventName,
): boolean {
return discreteReplayableEvents.indexOf(eventType) > -1;
}

Expand Down Expand Up @@ -300,13 +302,6 @@ function accumulateOrCreateContinuousQueuedReplayableEvent(
return existingQueuedEvent;
}

export function isCapturePhaseSynchronouslyHydratableEvent(
eventName: DOMEventName,
) {
// TODO: maybe include more events
return isReplayableDiscreteEvent(eventName);
}

export function queueIfContinuousEvent(
blockedOn: null | Container | SuspenseInstance,
domEventName: DOMEventName,
Expand Down