Repository: facebook/react
Pull request: Test that discrete events that aren't hydratable do not propagate

## Summary

This test case shows that we don't propagate discrete events that aren't synchronously hydratable.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
patch: @@ -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);
+  });
 });

filename: packages/react-dom/src/events/ReactDOMEventListener.js
patch: @@ -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,
@@ -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
@@ -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(
@@ -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);
@@ -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,

filename: packages/react-dom/src/events/ReactDOMEventReplaying.js
patch: @@ -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;
 }
 
@@ -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,