Skip to content

Commit

Permalink
Test that discrete events that aren't hydratable do not propagate (#2…
Browse files Browse the repository at this point in the history
…2502)

* test that discrete events that arent hydratable do not propagate

* lint

* feedback

* feedback

* lint

* better test

* nits

* lint
  • Loading branch information
salazarm authored Oct 6, 2021
1 parent 579c008 commit 6ecad79
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 18 deletions.
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

0 comments on commit 6ecad79

Please sign in to comment.