From 0a527707cd8befd21a741ca9646a8551842190b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 23 Sep 2019 11:21:10 -0700 Subject: [PATCH] Event Replaying (#16725) * Add Event Replaying Infra * Wire up Roots and Suspense boundaries, to retry events, after they commit * Replay discrete events in order in a separate scheduler callback * Add continuous events These events only replay their last target if the target is not yet hydrated. That way we don't have to wait for a previously hovered boundary before invoking the current target. * Enable tests from before These tests were written with replaying in mind and now we can properly enable them. * Unify replaying and dispatching * Mark system flags as a replay and pass to legacy events That way we can check if this is a replay and therefore needs a special case. One such special case is "mouseover" where we check the relatedTarget. * Eagerly listen to all replayable events To minimize breakages in a minor, I only do this for the new root APIs since replaying only matters there anyway. Only if hydrating. For Flare, I have to attach all active listeners since the current system has one DOM listener for each. In a follow up I plan on optimizing that by only attaching one if there's at least one active listener which would allow us to start with only passive and then upgrade. * Desperate attempt to save bytese * Add test for mouseover replaying We need to check if the "relatedTarget" is mounted due to how the old event system dispatches from the "out" event. * Fix for nested boundaries and suspense in root container This is a follow up to #16673 which didn't have a test because it wasn't observable yet. This shows that it had a bug. * Rename RESPONDER_EVENT_SYSTEM to PLUGIN_EVENT_SYSTEM --- packages/legacy-events/EventPluginHub.js | 5 + packages/legacy-events/EventSystemFlags.js | 1 + packages/legacy-events/PluginModuleType.js | 2 + .../legacy-events/ResponderEventPlugin.js | 1 + .../ResponderEventPlugin-test.internal.js | 2 + ...DOMServerPartialHydration-test.internal.js | 458 +++++++++++++++- .../ReactServerRenderingHydration-test.js | 30 +- packages/react-dom/src/client/ReactDOM.js | 32 +- .../src/client/ReactDOMComponentTree.js | 28 +- .../src/client/ReactDOMHostConfig.js | 19 + .../src/events/BeforeInputEventPlugin.js | 1 + .../react-dom/src/events/ChangeEventPlugin.js | 1 + .../src/events/EnterLeaveEventPlugin.js | 23 +- .../src/events/ReactBrowserEventEmitter.js | 80 +-- .../src/events/ReactDOMEventListener.js | 147 +++++- .../src/events/ReactDOMEventReplaying.js | 487 ++++++++++++++++++ .../react-dom/src/events/SelectEventPlugin.js | 1 + .../react-dom/src/events/SimpleEventPlugin.js | 2 + .../src/ReactFabricEventEmitter.js | 2 + .../src/ReactNativeBridgeEventPlugin.js | 2 + .../src/ReactNativeEventEmitter.js | 2 + .../src/ReactFiberCommitWork.js | 43 +- .../src/ReactFiberTreeReflection.js | 27 + .../src/forks/ReactFiberHostConfig.custom.js | 3 + packages/shared/HostConfigWithNoHydration.js | 2 + 25 files changed, 1270 insertions(+), 131 deletions(-) create mode 100644 packages/react-dom/src/events/ReactDOMEventReplaying.js diff --git a/packages/legacy-events/EventPluginHub.js b/packages/legacy-events/EventPluginHub.js index 297d0cbdcc44..1ab4bc3fa456 100644 --- a/packages/legacy-events/EventPluginHub.js +++ b/packages/legacy-events/EventPluginHub.js @@ -22,6 +22,7 @@ import type {ReactSyntheticEvent} from './ReactSyntheticEventType'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {AnyNativeEvent} from './PluginModuleType'; import type {TopLevelType} from './TopLevelEventTypes'; +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; function isInteractive(tag) { return ( @@ -131,6 +132,7 @@ export function getListener(inst: Fiber, registrationName: string) { */ function extractPluginEvents( topLevelType: TopLevelType, + eventSystemFlags: EventSystemFlags, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, @@ -142,6 +144,7 @@ function extractPluginEvents( if (possiblePlugin) { const extractedEvents = possiblePlugin.extractEvents( topLevelType, + eventSystemFlags, targetInst, nativeEvent, nativeEventTarget, @@ -156,12 +159,14 @@ function extractPluginEvents( export function runExtractedPluginEventsInBatch( topLevelType: TopLevelType, + eventSystemFlags: EventSystemFlags, targetInst: null | Fiber, nativeEvent: AnyNativeEvent, nativeEventTarget: EventTarget, ) { const events = extractPluginEvents( topLevelType, + eventSystemFlags, targetInst, nativeEvent, nativeEventTarget, diff --git a/packages/legacy-events/EventSystemFlags.js b/packages/legacy-events/EventSystemFlags.js index be5e3544a2ea..1ad8aa01c251 100644 --- a/packages/legacy-events/EventSystemFlags.js +++ b/packages/legacy-events/EventSystemFlags.js @@ -14,3 +14,4 @@ export const RESPONDER_EVENT_SYSTEM = 1 << 1; export const IS_PASSIVE = 1 << 2; export const IS_ACTIVE = 1 << 3; export const PASSIVE_NOT_SUPPORTED = 1 << 4; +export const IS_REPLAYED = 1 << 5; diff --git a/packages/legacy-events/PluginModuleType.js b/packages/legacy-events/PluginModuleType.js index cd7a07661ab4..c1cf5fc6783e 100644 --- a/packages/legacy-events/PluginModuleType.js +++ b/packages/legacy-events/PluginModuleType.js @@ -13,6 +13,7 @@ import type { ReactSyntheticEvent, } from './ReactSyntheticEventType'; import type {TopLevelType} from './TopLevelEventTypes'; +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; export type EventTypes = {[key: string]: DispatchConfig}; @@ -24,6 +25,7 @@ export type PluginModule = { eventTypes: EventTypes, extractEvents: ( topLevelType: TopLevelType, + eventSystemFlags: EventSystemFlags, targetInst: null | Fiber, nativeTarget: NativeEvent, nativeEventTarget: EventTarget, diff --git a/packages/legacy-events/ResponderEventPlugin.js b/packages/legacy-events/ResponderEventPlugin.js index 235e68829947..aea49578a397 100644 --- a/packages/legacy-events/ResponderEventPlugin.js +++ b/packages/legacy-events/ResponderEventPlugin.js @@ -504,6 +504,7 @@ const ResponderEventPlugin = { */ extractEvents: function( topLevelType, + eventSystemFlags, targetInst, nativeEvent, nativeEventTarget, diff --git a/packages/legacy-events/__tests__/ResponderEventPlugin-test.internal.js b/packages/legacy-events/__tests__/ResponderEventPlugin-test.internal.js index c1959659a1db..c235578b2875 100644 --- a/packages/legacy-events/__tests__/ResponderEventPlugin-test.internal.js +++ b/packages/legacy-events/__tests__/ResponderEventPlugin-test.internal.js @@ -10,6 +10,7 @@ 'use strict'; const {HostComponent} = require('shared/ReactWorkTags'); +const {PLUGIN_EVENT_SYSTEM} = require('legacy-events/EventSystemFlags'); let EventBatching; let EventPluginUtils; @@ -313,6 +314,7 @@ const run = function(config, hierarchyConfig, nativeEventConfig) { // Trigger the event const extractedEvents = ResponderEventPlugin.extractEvents( nativeEventConfig.topLevelType, + PLUGIN_EVENT_SYSTEM, nativeEventConfig.targetInst, nativeEventConfig.nativeEvent, nativeEventConfig.target, diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index f4e465ea4d39..dffe284c69b2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -17,6 +17,58 @@ let ReactFeatureFlags; let Suspense; let SuspenseList; let act; +let useHover; + +function dispatchMouseEvent(to, from) { + if (!to) { + to = null; + } + if (!from) { + from = null; + } + if (from) { + const mouseOutEvent = document.createEvent('MouseEvents'); + mouseOutEvent.initMouseEvent( + 'mouseout', + true, + true, + window, + 0, + 50, + 50, + 50, + 50, + false, + false, + false, + false, + 0, + to, + ); + from.dispatchEvent(mouseOutEvent); + } + if (to) { + const mouseOverEvent = document.createEvent('MouseEvents'); + mouseOverEvent.initMouseEvent( + 'mouseover', + true, + true, + window, + 0, + 50, + 50, + 50, + 50, + false, + false, + false, + false, + 0, + from, + ); + to.dispatchEvent(mouseOverEvent); + } +} describe('ReactDOMServerPartialHydration', () => { beforeEach(() => { @@ -34,6 +86,8 @@ describe('ReactDOMServerPartialHydration', () => { Scheduler = require('scheduler'); Suspense = React.Suspense; SuspenseList = React.unstable_SuspenseList; + + useHover = require('react-interactions/events/hover').useHover; }); it('hydrates a parent even if a child Suspense boundary is blocked', async () => { @@ -1805,12 +1859,6 @@ describe('ReactDOMServerPartialHydration', () => { Scheduler.unstable_flushAll(); jest.runAllTimers(); - // TODO: With selective hydration the event should've been replayed - // but for now we'll have to issue it again. - act(() => { - a.click(); - }); - expect(clicks).toBe(1); expect(container.textContent).toBe('Hello'); @@ -1868,6 +1916,12 @@ describe('ReactDOMServerPartialHydration', () => { suspend = true; let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); root.render(); + + // We'll do one click before hydrating. + a.click(); + // This should be delayed. + expect(onEvent).toHaveBeenCalledTimes(0); + Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -1885,13 +1939,165 @@ describe('ReactDOMServerPartialHydration', () => { Scheduler.unstable_flushAll(); jest.runAllTimers(); - // TODO: With selective hydration the event should've been replayed - // but for now we'll have to issue it again. - act(() => { - a.click(); + expect(onEvent).toHaveBeenCalledTimes(2); + + document.body.removeChild(container); + }); + + it('invokes discrete events on nested suspense boundaries in a root (legacy system)', async () => { + let suspend = false; + let resolve; + let promise = new Promise(resolvePromise => (resolve = resolvePromise)); + + let clicks = 0; + + function Button() { + return ( + { + clicks++; + }}> + Click me + + ); + } + + function Child() { + if (suspend) { + throw promise; + } else { + return ( + +