From 434adcc7620e262b5dd93068513a1975dc9a5782 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 15:17:54 -0800 Subject: [PATCH 01/18] Implement addEventListener and removeEventListener on Fabric HostComponent --- .../src/ReactFabricEventEmitter.js | 4 +- .../src/ReactFabricHostConfig.js | 119 ++++++++++++++++++ .../src/ReactNativeBridgeEventPlugin.js | 34 +++-- .../src/ReactNativeEventEmitter.js | 4 +- .../src/ReactNativeGetListener.js | 36 ------ 5 files changed, 145 insertions(+), 52 deletions(-) delete mode 100644 packages/react-native-renderer/src/ReactNativeGetListener.js diff --git a/packages/react-native-renderer/src/ReactFabricEventEmitter.js b/packages/react-native-renderer/src/ReactFabricEventEmitter.js index d8492156fdc7c..c3b89e91779a3 100644 --- a/packages/react-native-renderer/src/ReactFabricEventEmitter.js +++ b/packages/react-native-renderer/src/ReactFabricEventEmitter.js @@ -18,12 +18,12 @@ import {batchedUpdates} from './legacy-events/ReactGenericBatching'; import accumulateInto from './legacy-events/accumulateInto'; import {plugins} from './legacy-events/EventPluginRegistry'; -import getListener from './ReactNativeGetListener'; +import getListeners from './ReactNativeGetListeners'; import {runEventsInBatch} from './legacy-events/EventBatching'; import {RawEventEmitter} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; -export {getListener, registrationNameModules as registrationNames}; +export {getListeners, registrationNameModules as registrationNames}; /** * Allows registered plugins an opportunity to extract events from top-level diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 727b782efd768..68c60a0d8a84a 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -95,6 +95,28 @@ export type RendererInspectionConfig = $ReadOnly<{| ) => void, |}>; +// TODO?: find a better place for this type to live +export type EventListenerOptions = $ReadOnly<{| + capture?: boolean, + once?: boolean, + passive?: boolean, + signal: mixed, // not yet implemented +|}>; +export type EventListenerRemoveOptions = $ReadOnly<{| + capture?: boolean, +|}>; + +// TODO?: this will be changed in the future to be w3c-compatible and allow "EventListener" objects as well as functions. +export type EventListener = Function; + +type InternalEventListeners = { + [string]: {| + listener: EventListener, + options: EventListenerOptions, + invalidated: boolean, + |}[], +}; + // TODO: Remove this conditional once all changes have propagated. if (registerEventHandler) { /** @@ -111,6 +133,7 @@ class ReactFabricHostComponent { viewConfig: ViewConfig; currentProps: Props; _internalInstanceHandle: Object; + _eventListeners: ?InternalEventListeners; constructor( tag: number, @@ -193,6 +216,102 @@ class ReactFabricHostComponent { return; } + + // This API (addEventListener, removeEventListener) attempts to adhere to the + // w3 Level2 Events spec as much as possible, treating HostComponent as a DOM node. + // + // Unless otherwise noted, these methods should "just work" and adhere to the W3 specs. + // If they deviate in a way that is not explicitly noted here, you've found a bug! + // + // See: + // * https://www.w3.org/TR/DOM-Level-2-Events/events.html + // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener + // + // And notably, not implemented (yet?): + // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent + // + // + // Deviations from spec/TODOs: + // (1) listener must currently be a function, we do not support EventListener objects yet. + // (2) we do not support the `signal` option / AbortSignal yet + addEventListener_unstable( + eventType: string, + listener: EventListener, + options: EventListenerOptions | boolean, + ) { + if (typeof eventType !== 'string') { + throw new Error('addEventListener_unstable eventType must be a string'); + } + if (typeof listener !== 'function') { + throw new Error('addEventListener_unstable listener must be a function'); + } + + // The third argument is either boolean indicating "captures" or an object. + const optionsObj = + typeof options === 'object' && options !== null ? options : {}; + const capture = + (typeof options === 'boolean' ? options : optionsObj.capture) || false; + const once = optionsObj.once || false; + const passive = optionsObj.passive || false; + const signal = null; // TODO: implement signal/AbortSignal + + const eventListeners: InternalEventListeners = this._eventListeners || {}; + if (this._eventListeners === null) { + this._eventListeners = eventListeners; + } + + const namedEventListeners = eventListeners[eventType] || []; + if (eventListeners[eventType] == null) { + eventListeners[eventType] = namedEventListeners; + } + + namedEventListeners.push({ + listener: listener, + invalidated: false, + options: { + capture: capture, + once: once, + passive: passive, + signal: signal, + }, + }); + } + + // See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener + removeEventListener_unstable( + eventType: string, + listener: EventListener, + options: EventListenerRemoveOptions | boolean, + ) { + // eventType and listener must be referentially equal to be removed from the listeners + // data structure, but in "options" we only check the `capture` flag, according to spec. + // That means if you add the same function as a listener with capture set to true and false, + // you must also call removeEventListener twice with capture set to true/false. + const optionsObj = + typeof options === 'object' && options !== null ? options : {}; + const capture = + (typeof options === 'boolean' ? options : optionsObj.capture) || false; + + // If there are no event listeners or named event listeners, we can bail early - our + // job is already done. + const eventListeners = this._eventListeners; + if (!eventListeners) { + return; + } + const namedEventListeners = eventListeners[eventType]; + if (!namedEventListeners) { + return; + } + + // TODO: optimize this path to make remove cheaper + eventListeners[eventType] = namedEventListeners.filter(listenerObj => { + return !( + listenerObj.listener === listener && + listenerObj.options.capture === capture + ); + }); + } } // eslint-disable-next-line no-unused-expressions diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 212b181c08294..bea46852b9741 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -10,11 +10,12 @@ import type {AnyNativeEvent} from './legacy-events/PluginModuleType'; import type {TopLevelType} from './legacy-events/TopLevelEventTypes'; import SyntheticEvent from './legacy-events/SyntheticEvent'; +import type {PropagationPhases} from './legacy-events/PropagationPhases'; // Module provided by RN: import {ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; import accumulateInto from './legacy-events/accumulateInto'; -import getListener from './ReactNativeGetListener'; +import getListeners from './ReactNativeGetListeners'; import forEachAccumulated from './legacy-events/forEachAccumulated'; import {HostComponent} from 'react-reconciler/src/ReactWorkTags'; @@ -26,10 +27,10 @@ const { // Start of inline: the below functions were inlined from // EventPropagator.js, as they deviated from ReactDOM's newer // implementations. -function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) { +function listenersAtPhase(inst, event, propagationPhase: PropagationPhases) { const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; - return getListener(inst, registrationName); + return getListeners(inst, registrationName, propagationPhase); } function accumulateDirectionalDispatches(inst, phase, event) { @@ -38,13 +39,16 @@ function accumulateDirectionalDispatches(inst, phase, event) { console.error('Dispatching inst must not be null'); } } - const listener = listenerAtPhase(inst, event, phase); - if (listener) { + const listeners = listenersAtPhase(inst, event, phase); + if (listeners && listeners.length > 0) { event._dispatchListeners = accumulateInto( event._dispatchListeners, - listener, + listeners, ); - event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); + const insts = listeners.map(() => { + return inst; + }); + event._dispatchInstances = accumulateInto(event._dispatchInstances, insts); } } @@ -103,13 +107,20 @@ function accumulateDispatches( ): void { if (inst && event && event.dispatchConfig.registrationName) { const registrationName = event.dispatchConfig.registrationName; - const listener = getListener(inst, registrationName); - if (listener) { + const listeners = getListeners(inst, registrationName, 'bubbled'); + if (listeners) { event._dispatchListeners = accumulateInto( event._dispatchListeners, - listener, + listeners, + ); + // an inst for every listener + const insts = listeners.map(() => { + return inst; + }); + event._dispatchInstances = accumulateInto( + event._dispatchInstances, + insts, ); - event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } } } @@ -130,7 +141,6 @@ function accumulateDirectDispatches(events: ?(Array | Object)) { } // End of inline -type PropagationPhases = 'bubbled' | 'captured'; const ReactNativeBridgeEventPlugin = { eventTypes: {}, diff --git a/packages/react-native-renderer/src/ReactNativeEventEmitter.js b/packages/react-native-renderer/src/ReactNativeEventEmitter.js index 2ba35aed39b9f..91816a53f82da 100644 --- a/packages/react-native-renderer/src/ReactNativeEventEmitter.js +++ b/packages/react-native-renderer/src/ReactNativeEventEmitter.js @@ -17,12 +17,12 @@ import {registrationNameModules} from './legacy-events/EventPluginRegistry'; import {batchedUpdates} from './legacy-events/ReactGenericBatching'; import {runEventsInBatch} from './legacy-events/EventBatching'; import {plugins} from './legacy-events/EventPluginRegistry'; -import getListener from './ReactNativeGetListener'; +import getListeners from './ReactNativeGetListeners'; import accumulateInto from './legacy-events/accumulateInto'; import {getInstanceFromNode} from './ReactNativeComponentTree'; -export {getListener, registrationNameModules as registrationNames}; +export {getListeners, registrationNameModules as registrationNames}; /** * Version of `ReactBrowserEventEmitter` that works on the receiving side of a diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js deleted file mode 100644 index 4f76fddd29e7b..0000000000000 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * @flow - */ - -import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; - -import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils'; - -export default function getListener( - inst: Fiber, - registrationName: string, -): Function | null { - const stateNode = inst.stateNode; - if (stateNode === null) { - // Work in progress (ex: onload events in incremental mode). - return null; - } - const props = getFiberCurrentPropsFromNode(stateNode); - if (props === null) { - // Work in progress. - return null; - } - const listener = props[registrationName]; - - if (listener && typeof listener !== 'function') { - throw new Error( - `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`, - ); - } - - return listener; -} From fc44017809323478ff9b7fe692a325298e1fcc01 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 15:18:04 -0800 Subject: [PATCH 02/18] add files --- .../src/ReactNativeGetListeners.js | 145 ++++++++++++++++++ .../src/legacy-events/PropagationPhases.js | 10 ++ 2 files changed, 155 insertions(+) create mode 100644 packages/react-native-renderer/src/ReactNativeGetListeners.js create mode 100644 packages/react-native-renderer/src/legacy-events/PropagationPhases.js diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js new file mode 100644 index 0000000000000..3fd601c726721 --- /dev/null +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -0,0 +1,145 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @flow + */ + +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import type {PropagationPhases} from './legacy-events/PropagationPhases'; + +import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils'; + +/** + * Get a list of listeners for a specific event, in-order. + * For React Native we treat the props-based function handlers + * as the first-class citizens, and they are always executed first + * for both capture and bubbling phase. + * + * We need "phase" propagated to this point to support the HostComponent + * EventEmitter API, which does not mutate the name of the handler based + * on phase (whereas prop handlers are registered as `onMyEvent` and `onMyEvent_Capture`). + * + * Native system events emitted into React Native + * will be emitted both to the prop handler function and to imperative event + * listeners. + */ +export default function getListeners( + inst: Fiber, + registrationName: string, + phase: PropagationPhases, + isCustomEvent: boolean, +): Array | null { + // Previously, there was only one possible listener for an event: + // the onEventName property in props. + // Now, it is also possible to have N listeners + // for a specific event on a node. Thus, we accumulate all of the listeners, + // including the props listener, and return a function that calls them all in + // order, starting with the handler prop and then the listeners in order. + // We return either a non-empty array or null. + let listeners = null; + + const stateNode = inst.stateNode; + + if (stateNode === null) { + return null; + } + + // If null: Work in progress (ex: onload events in incremental mode). + if (!isCustomEvent) { + const props = getFiberCurrentPropsFromNode(stateNode); + if (props === null) { + // Work in progress. + return null; + } + const listener = props[registrationName]; + + if (listener && typeof listener !== 'function') { + throw new Error( + `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`, + ); + } + + if (listener) { + if (listeners === null) { + listeners = []; + } + listeners.push(listener); + } + } + + // Get imperative event listeners for this event + if ( + stateNode.canonical && + stateNode.canonical._eventListeners && + stateNode.canonical._eventListeners[registrationName] && + stateNode.canonical._eventListeners[registrationName].length > 0 + ) { + // TODO: for now, all of these events get an `rn:` prefix to enforce + // that the user knows they're only getting non-W3C-compliant events. + // Events might not necessarily be noncompliant, but we currently have + // no verification that /any/ events are compliant. + // Thus, we prefix to ensure no collision with W3C event names. + const eventListeners = + stateNode.canonical._eventListeners['rn:' + registrationName]; + const requestedPhaseIsCapture = phase === 'captured'; + + eventListeners.forEach(listenerObj => { + // Make sure phase of listener matches requested phase + const isCaptureEvent = + listenerObj.options.capture != null && listenerObj.options.capture; + if (isCaptureEvent !== requestedPhaseIsCapture) { + return; + } + + // For now (this is an area of future optimization) we must wrap + // all imperative event listeners in a function to unwrap the SyntheticEvent + // and pass them an Event. + // When this API is more stable and used more frequently, we can revisit. + const listenerFnWrapper = function () { + const args = Array.prototype.slice.call(arguments); + const syntheticEvent = args[0]; + + const eventInst = new CustomEvent(registrationName, { detail: syntheticEvent.nativeEvent }); + eventInst.isTrusted = true; + eventInst.setSyntheticEvent(syntheticEvent); + + listenerObj.listener(eventInst); + }; + + if (listeners === null) { + listeners = []; + } + + // Only call once? + // If so, we ensure that it's only called once by setting a flag + // and by removing it from eventListeners once it is called (but only + // when it's actually been executed). + if (listenerObj.options.once) { + listeners.push(function() { + const args = Array.prototype.slice.call(arguments); + + // Guard against function being called more than once in + // case there are somehow multiple in-flight references to + // it being processed + if (!listenerObj.invalidated) { + listenerObj.listener.apply(null, args); + listenerObj.invalidated = true; + } + + // Remove from the event listener once it's been called + stateNode.canonical.removeEventListener_unstable( + registrationName, + listenerFnWrapper, + listenerObj.capture, + ); + }); + } else { + listeners.push(listenerFnWrapper); + } + }); + } + + return listeners; +} diff --git a/packages/react-native-renderer/src/legacy-events/PropagationPhases.js b/packages/react-native-renderer/src/legacy-events/PropagationPhases.js new file mode 100644 index 0000000000000..7d05d30be8c47 --- /dev/null +++ b/packages/react-native-renderer/src/legacy-events/PropagationPhases.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type PropagationPhases = 'bubbled' | 'captured'; From b85203552491594d9fe9107ed2165bfdf27576e6 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 15:19:16 -0800 Subject: [PATCH 03/18] re-add CustomEvent --- .../Libraries/ReactPrivate/CustomEvent.js | 14 ++++++++++++++ .../ReactPrivate/ReactNativePrivateInterface.js | 3 +++ 2 files changed, 17 insertions(+) create mode 100644 packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js new file mode 100644 index 0000000000000..0b0b79c5ddddf --- /dev/null +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +// See the react-native repository for a full implementation. +// This is just a stub, currently to pass `instanceof` checks. +const CustomEvent = jest.fn(); + +module.exports = {default: CustomEvent}; diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js index 3eaf3a5a38057..dbfb9910c943e 100644 --- a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js @@ -44,4 +44,7 @@ module.exports = { get RawEventEmitter() { return require('./RawEventEmitter').default; }, + get CustomEvent() { + return require('./CustomEvent').default; + }, }; From cbe9457a3f85361b76809320cf433fe10a601ee4 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 16:05:13 -0800 Subject: [PATCH 04/18] fix flow --- .../src/ReactNativeBridgeEventPlugin.js | 4 +-- .../src/ReactNativeGetListeners.js | 36 ++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index bea46852b9741..891b89e24b079 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -30,7 +30,7 @@ const { function listenersAtPhase(inst, event, propagationPhase: PropagationPhases) { const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; - return getListeners(inst, registrationName, propagationPhase); + return getListeners(inst, registrationName, propagationPhase, true); } function accumulateDirectionalDispatches(inst, phase, event) { @@ -107,7 +107,7 @@ function accumulateDispatches( ): void { if (inst && event && event.dispatchConfig.registrationName) { const registrationName = event.dispatchConfig.registrationName; - const listeners = getListeners(inst, registrationName, 'bubbled'); + const listeners = getListeners(inst, registrationName, 'bubbled', false); if (listeners) { event._dispatchListeners = accumulateInto( event._dispatchListeners, diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 3fd601c726721..a8def491a7787 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -47,26 +47,25 @@ export default function getListeners( } // If null: Work in progress (ex: onload events in incremental mode). - if (!isCustomEvent) { - const props = getFiberCurrentPropsFromNode(stateNode); - if (props === null) { - // Work in progress. - return null; - } - const listener = props[registrationName]; + const props = getFiberCurrentPropsFromNode(stateNode); + if (props === null) { + // Work in progress. + return null; + } - if (listener && typeof listener !== 'function') { - throw new Error( - `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`, - ); - } + const listener = props[registrationName]; - if (listener) { - if (listeners === null) { - listeners = []; - } - listeners.push(listener); + if (listener && typeof listener !== 'function') { + throw new Error( + `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`, + ); + } + + if (listener) { + if (listeners === null) { + listeners = []; } + listeners.push(listener); } // Get imperative event listeners for this event @@ -103,6 +102,9 @@ export default function getListeners( const eventInst = new CustomEvent(registrationName, { detail: syntheticEvent.nativeEvent }); eventInst.isTrusted = true; + // setSyntheticEvent is present on the React Native Event shim. + // It is used to forward method calls on Event to the underlying SyntheticEvent. + // $FlowFixMe eventInst.setSyntheticEvent(syntheticEvent); listenerObj.listener(eventInst); From a85bdf57dc078ac7848678728bce071718de89c6 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 19:05:04 -0800 Subject: [PATCH 05/18] Need to get CustomEvent from an import since it won't exist on the global scope by default --- .../react-native-renderer/src/ReactNativeGetListeners.js | 6 +++++- scripts/flow/react-native-host-hooks.js | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index a8def491a7787..34da745122bbf 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -10,6 +10,9 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {PropagationPhases} from './legacy-events/PropagationPhases'; import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils'; +import { + CustomEvent +} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; /** * Get a list of listeners for a specific event, in-order. @@ -29,7 +32,7 @@ export default function getListeners( inst: Fiber, registrationName: string, phase: PropagationPhases, - isCustomEvent: boolean, + dispatchToImperativeListeners: boolean, ): Array | null { // Previously, there was only one possible listener for an event: // the onEventName property in props. @@ -70,6 +73,7 @@ export default function getListeners( // Get imperative event listeners for this event if ( + dispatchToImperativeListeners && stateNode.canonical && stateNode.canonical._eventListeners && stateNode.canonical._eventListeners[registrationName] && diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index c7a2fc8dc3212..896964f063643 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -138,6 +138,7 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface' emit: (channel: string, event: RawEventEmitterEvent) => string, ... }; + declare export var CustomEvent: CustomEvent; } declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' { From 4dbbf5a93e309c724e63379073e3237cd623a8b0 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 19:11:25 -0800 Subject: [PATCH 06/18] yarn prettier-all --- .../src/ReactNativeGetListeners.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 34da745122bbf..496bc960779de 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -10,9 +10,7 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {PropagationPhases} from './legacy-events/PropagationPhases'; import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils'; -import { - CustomEvent -} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; +import {CustomEvent} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; /** * Get a list of listeners for a specific event, in-order. @@ -100,11 +98,13 @@ export default function getListeners( // all imperative event listeners in a function to unwrap the SyntheticEvent // and pass them an Event. // When this API is more stable and used more frequently, we can revisit. - const listenerFnWrapper = function () { + const listenerFnWrapper = function() { const args = Array.prototype.slice.call(arguments); const syntheticEvent = args[0]; - const eventInst = new CustomEvent(registrationName, { detail: syntheticEvent.nativeEvent }); + const eventInst = new CustomEvent(registrationName, { + detail: syntheticEvent.nativeEvent, + }); eventInst.isTrusted = true; // setSyntheticEvent is present on the React Native Event shim. // It is used to forward method calls on Event to the underlying SyntheticEvent. From f5c518d10155ab4e3951ab6b301d210e29f2315c Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 19:58:45 -0800 Subject: [PATCH 07/18] use a mangled name consistently to refer to imperatively registered event handlers --- .../src/ReactNativeGetListeners.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 496bc960779de..076001c027e4d 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -69,21 +69,24 @@ export default function getListeners( listeners.push(listener); } + // TODO: for now, all of these events get an `rn:` prefix to enforce + // that the user knows they're only getting non-W3C-compliant events + // through this imperative event API. + // Events might not necessarily be noncompliant, but we currently have + // no verification that /any/ events are compliant. + // Thus, we prefix to ensure no collision with W3C event names. + const mangledImperativeRegistrationName = 'rn:' + registrationName; + // Get imperative event listeners for this event if ( dispatchToImperativeListeners && stateNode.canonical && stateNode.canonical._eventListeners && - stateNode.canonical._eventListeners[registrationName] && - stateNode.canonical._eventListeners[registrationName].length > 0 + stateNode.canonical._eventListeners[mangledImperativeRegistrationName] && + stateNode.canonical._eventListeners[mangledImperativeRegistrationName].length > 0 ) { - // TODO: for now, all of these events get an `rn:` prefix to enforce - // that the user knows they're only getting non-W3C-compliant events. - // Events might not necessarily be noncompliant, but we currently have - // no verification that /any/ events are compliant. - // Thus, we prefix to ensure no collision with W3C event names. const eventListeners = - stateNode.canonical._eventListeners['rn:' + registrationName]; + stateNode.canonical._eventListeners[mangledImperativeRegistrationName]; const requestedPhaseIsCapture = phase === 'captured'; eventListeners.forEach(listenerObj => { @@ -102,7 +105,7 @@ export default function getListeners( const args = Array.prototype.slice.call(arguments); const syntheticEvent = args[0]; - const eventInst = new CustomEvent(registrationName, { + const eventInst = new CustomEvent(mangledImperativeRegistrationName, { detail: syntheticEvent.nativeEvent, }); eventInst.isTrusted = true; @@ -136,7 +139,7 @@ export default function getListeners( // Remove from the event listener once it's been called stateNode.canonical.removeEventListener_unstable( - registrationName, + mangledImperativeRegistrationName, listenerFnWrapper, listenerObj.capture, ); From f4b835a4022af95ba68d015c32d63872831473d5 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 20:00:30 -0800 Subject: [PATCH 08/18] yarn prettier-all --- packages/react-native-renderer/src/ReactNativeGetListeners.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 076001c027e4d..fbfc56b0d7977 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -83,7 +83,8 @@ export default function getListeners( stateNode.canonical && stateNode.canonical._eventListeners && stateNode.canonical._eventListeners[mangledImperativeRegistrationName] && - stateNode.canonical._eventListeners[mangledImperativeRegistrationName].length > 0 + stateNode.canonical._eventListeners[mangledImperativeRegistrationName] + .length > 0 ) { const eventListeners = stateNode.canonical._eventListeners[mangledImperativeRegistrationName]; From 4ce290c8da578bc09a63514dd4b75f737cc836f2 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 20:13:19 -0800 Subject: [PATCH 09/18] fuzzy null check --- packages/react-native-renderer/src/ReactFabricHostConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 68c60a0d8a84a..81295f6e50f68 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -257,7 +257,7 @@ class ReactFabricHostComponent { const signal = null; // TODO: implement signal/AbortSignal const eventListeners: InternalEventListeners = this._eventListeners || {}; - if (this._eventListeners === null) { + if (this._eventListeners == null) { this._eventListeners = eventListeners; } From a1cd4725e264d501dcf4d00bf08913dbe193c924 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 20:49:02 -0800 Subject: [PATCH 10/18] fix capture phase event listener logic --- .../react-native-renderer/src/ReactNativeGetListeners.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index fbfc56b0d7977..8c75e4a2205e1 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -75,7 +75,8 @@ export default function getListeners( // Events might not necessarily be noncompliant, but we currently have // no verification that /any/ events are compliant. // Thus, we prefix to ensure no collision with W3C event names. - const mangledImperativeRegistrationName = 'rn:' + registrationName; + const requestedPhaseIsCapture = phase === 'captured'; + const mangledImperativeRegistrationName = requestedPhaseIsCapture ? 'rn:' + registrationName.replace(/Capture$/, '') : 'rn:' + registrationName; // Get imperative event listeners for this event if ( @@ -83,12 +84,10 @@ export default function getListeners( stateNode.canonical && stateNode.canonical._eventListeners && stateNode.canonical._eventListeners[mangledImperativeRegistrationName] && - stateNode.canonical._eventListeners[mangledImperativeRegistrationName] - .length > 0 + stateNode.canonical._eventListeners[mangledImperativeRegistrationName].length > 0 ) { const eventListeners = stateNode.canonical._eventListeners[mangledImperativeRegistrationName]; - const requestedPhaseIsCapture = phase === 'captured'; eventListeners.forEach(listenerObj => { // Make sure phase of listener matches requested phase From c135e37ae0735c73fa44623b09dab3e076c6a9b9 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 20:51:20 -0800 Subject: [PATCH 11/18] early exit from getEventListeners more often --- .../src/ReactNativeGetListeners.js | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 8c75e4a2205e1..2cc387d51f687 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -69,6 +69,17 @@ export default function getListeners( listeners.push(listener); } + // If there are no imperative listeners, early exit. + if ( + !( + dispatchToImperativeListeners && + stateNode.canonical && + stateNode.canonical._eventListeners + ) + ) { + return listeners; + } + // TODO: for now, all of these events get an `rn:` prefix to enforce // that the user knows they're only getting non-W3C-compliant events // through this imperative event API. @@ -76,15 +87,15 @@ export default function getListeners( // no verification that /any/ events are compliant. // Thus, we prefix to ensure no collision with W3C event names. const requestedPhaseIsCapture = phase === 'captured'; - const mangledImperativeRegistrationName = requestedPhaseIsCapture ? 'rn:' + registrationName.replace(/Capture$/, '') : 'rn:' + registrationName; + const mangledImperativeRegistrationName = requestedPhaseIsCapture + ? 'rn:' + registrationName.replace(/Capture$/, '') + : 'rn:' + registrationName; // Get imperative event listeners for this event if ( - dispatchToImperativeListeners && - stateNode.canonical && - stateNode.canonical._eventListeners && stateNode.canonical._eventListeners[mangledImperativeRegistrationName] && - stateNode.canonical._eventListeners[mangledImperativeRegistrationName].length > 0 + stateNode.canonical._eventListeners[mangledImperativeRegistrationName] + .length > 0 ) { const eventListeners = stateNode.canonical._eventListeners[mangledImperativeRegistrationName]; From ea150534e957df5f34d7222badd7ab7ccd53131a Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 21:05:19 -0800 Subject: [PATCH 12/18] make some optimizations to getEventListeners and the bridge plugin --- .../src/ReactNativeBridgeEventPlugin.js | 47 +++++++++---------- .../src/ReactNativeGetListeners.js | 23 +++++---- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 891b89e24b079..e5b28a089db44 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -18,6 +18,7 @@ import accumulateInto from './legacy-events/accumulateInto'; import getListeners from './ReactNativeGetListeners'; import forEachAccumulated from './legacy-events/forEachAccumulated'; import {HostComponent} from 'react-reconciler/src/ReactWorkTags'; +import isArray from 'shared/isArray'; const { customBubblingEventTypes, @@ -33,6 +34,26 @@ function listenersAtPhase(inst, event, propagationPhase: PropagationPhases) { return getListeners(inst, registrationName, propagationPhase, true); } +function accumulateListenersAndInstances(inst, event, listeners) { + const listenersLength = listeners + ? isArray(listeners) + ? listeners.length + : 1 + : 0; + if (listenersLength > 0) { + event._dispatchListeners = accumulateInto( + event._dispatchListeners, + listeners, + ); + + // Avoid allocating additional arrays here + event._dispatchInstances = event._dispatchInstances || []; + for (let i = 0; i < listenersLength; i++) { + event._dispatchInstances.push(inst); + } + } +} + function accumulateDirectionalDispatches(inst, phase, event) { if (__DEV__) { if (!inst) { @@ -40,16 +61,7 @@ function accumulateDirectionalDispatches(inst, phase, event) { } } const listeners = listenersAtPhase(inst, event, phase); - if (listeners && listeners.length > 0) { - event._dispatchListeners = accumulateInto( - event._dispatchListeners, - listeners, - ); - const insts = listeners.map(() => { - return inst; - }); - event._dispatchInstances = accumulateInto(event._dispatchInstances, insts); - } + accumulateListenersAndInstances(inst, event, listeners); } function getParent(inst) { @@ -108,20 +120,7 @@ function accumulateDispatches( if (inst && event && event.dispatchConfig.registrationName) { const registrationName = event.dispatchConfig.registrationName; const listeners = getListeners(inst, registrationName, 'bubbled', false); - if (listeners) { - event._dispatchListeners = accumulateInto( - event._dispatchListeners, - listeners, - ); - // an inst for every listener - const insts = listeners.map(() => { - return inst; - }); - event._dispatchInstances = accumulateInto( - event._dispatchInstances, - insts, - ); - } + accumulateListenersAndInstances(inst, event, listeners); } } diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 2cc387d51f687..5242e2d116a8a 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -25,13 +25,16 @@ import {CustomEvent} from 'react-native/Libraries/ReactPrivate/ReactNativePrivat * Native system events emitted into React Native * will be emitted both to the prop handler function and to imperative event * listeners. + * + * This will either return null, a single Function without an array, or + * an array of 2+ items. */ export default function getListeners( inst: Fiber, registrationName: string, phase: PropagationPhases, dispatchToImperativeListeners: boolean, -): Array | null { +): null | Function | Array { // Previously, there was only one possible listener for an event: // the onEventName property in props. // Now, it is also possible to have N listeners @@ -62,13 +65,6 @@ export default function getListeners( ); } - if (listener) { - if (listeners === null) { - listeners = []; - } - listeners.push(listener); - } - // If there are no imperative listeners, early exit. if ( !( @@ -77,7 +73,7 @@ export default function getListeners( stateNode.canonical._eventListeners ) ) { - return listeners; + return listener; } // TODO: for now, all of these events get an `rn:` prefix to enforce @@ -130,6 +126,9 @@ export default function getListeners( if (listeners === null) { listeners = []; + if (listener) { + listeners.push(listener); + } } // Only call once? @@ -159,7 +158,11 @@ export default function getListeners( listeners.push(listenerFnWrapper); } }); + + if (listeners != null) { + return listeners; + } } - return listeners; + return listener; } From 39a045159792239628a44042772bb41b8ad267a5 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 21:19:57 -0800 Subject: [PATCH 13/18] fix accumulateInto logic --- .../src/ReactNativeBridgeEventPlugin.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index e5b28a089db44..7e0134966ffd3 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -47,9 +47,13 @@ function accumulateListenersAndInstances(inst, event, listeners) { ); // Avoid allocating additional arrays here - event._dispatchInstances = event._dispatchInstances || []; - for (let i = 0; i < listenersLength; i++) { - event._dispatchInstances.push(inst); + if (event._dispatchInstances == null && listenersLength === 1) { + event._dispatchInstances = inst; + } else { + event._dispatchInstances = event._dispatchInstances || []; + for (let i = 0; i < listenersLength; i++) { + event._dispatchInstances.push(inst); + } } } } From 7d5ecacc6bc419f51f007f2521339f89da24b299 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Mon, 28 Feb 2022 21:24:11 -0800 Subject: [PATCH 14/18] fix accumulateInto --- .../react-native-renderer/src/ReactNativeBridgeEventPlugin.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 7e0134966ffd3..2d74776e4200c 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -51,6 +51,9 @@ function accumulateListenersAndInstances(inst, event, listeners) { event._dispatchInstances = inst; } else { event._dispatchInstances = event._dispatchInstances || []; + if (!isArray(event._dispatchInstances)) { + event._dispatchInstances = [event._dispatchInstances]; + } for (let i = 0; i < listenersLength; i++) { event._dispatchInstances.push(inst); } From dd47e8780ea5612134a9986f688fad40d4abb394 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Tue, 1 Mar 2022 16:16:15 -0800 Subject: [PATCH 15/18] Simplifying getListeners at the expense of perf for the non-hot path --- .../src/ReactNativeGetListeners.js | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 5242e2d116a8a..d0c87d1d52d05 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -35,15 +35,6 @@ export default function getListeners( phase: PropagationPhases, dispatchToImperativeListeners: boolean, ): null | Function | Array { - // Previously, there was only one possible listener for an event: - // the onEventName property in props. - // Now, it is also possible to have N listeners - // for a specific event on a node. Thus, we accumulate all of the listeners, - // including the props listener, and return a function that calls them all in - // order, starting with the handler prop and then the listeners in order. - // We return either a non-empty array or null. - let listeners = null; - const stateNode = inst.stateNode; if (stateNode === null) { @@ -76,6 +67,24 @@ export default function getListeners( return listener; } + // Below this is the de-optimized path. + // If you are using _eventListeners, we do not (yet) + // expect this to be as performant as the props-only path. + // If/when this becomes a bottleneck, it can be refactored + // to avoid unnecessary closures and array allocations. + // + // Previously, there was only one possible listener for an event: + // the onEventName property in props. + // Now, it is also possible to have N listeners + // for a specific event on a node. Thus, we accumulate all of the listeners, + // including the props listener, and return a function that calls them all in + // order, starting with the handler prop and then the listeners in order. + // We return either a non-empty array or null. + const listeners = []; + if (listener) { + listeners.push(listener); + } + // TODO: for now, all of these events get an `rn:` prefix to enforce // that the user knows they're only getting non-W3C-compliant events // through this imperative event API. @@ -124,13 +133,6 @@ export default function getListeners( listenerObj.listener(eventInst); }; - if (listeners === null) { - listeners = []; - if (listener) { - listeners.push(listener); - } - } - // Only call once? // If so, we ensure that it's only called once by setting a flag // and by removing it from eventListeners once it is called (but only @@ -158,11 +160,14 @@ export default function getListeners( listeners.push(listenerFnWrapper); } }); + } - if (listeners != null) { - return listeners; - } + if (listeners.length === 0) { + return null; + } + if (listeners.length === 1) { + return listeners[0]; } - return listener; + return listeners; } From 9f22e9caa2e29306eb4317fbab16b4020b5ca792 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Tue, 1 Mar 2022 16:39:54 -0800 Subject: [PATCH 16/18] feedback --- .../src/ReactNativeGetListeners.js | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index d0c87d1d52d05..5e8e5213f9b4c 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -117,10 +117,7 @@ export default function getListeners( // all imperative event listeners in a function to unwrap the SyntheticEvent // and pass them an Event. // When this API is more stable and used more frequently, we can revisit. - const listenerFnWrapper = function() { - const args = Array.prototype.slice.call(arguments); - const syntheticEvent = args[0]; - + const listenerFnWrapper = function(syntheticEvent) { const eventInst = new CustomEvent(mangledImperativeRegistrationName, { detail: syntheticEvent.nativeEvent, }); @@ -138,23 +135,21 @@ export default function getListeners( // and by removing it from eventListeners once it is called (but only // when it's actually been executed). if (listenerObj.options.once) { - listeners.push(function() { - const args = Array.prototype.slice.call(arguments); + listeners.push(function(...args) { + // Remove from the event listener once it's been called + stateNode.canonical.removeEventListener_unstable( + mangledImperativeRegistrationName, + listenerFnWrapper, + listenerObj.capture, + ); // Guard against function being called more than once in // case there are somehow multiple in-flight references to // it being processed if (!listenerObj.invalidated) { - listenerObj.listener.apply(null, args); listenerObj.invalidated = true; + listenerObj.listener(...args); } - - // Remove from the event listener once it's been called - stateNode.canonical.removeEventListener_unstable( - mangledImperativeRegistrationName, - listenerFnWrapper, - listenerObj.capture, - ); }); } else { listeners.push(listenerFnWrapper); From 5045281a05d691786059cb40abf47e3a50091d69 Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Tue, 1 Mar 2022 19:17:15 -0800 Subject: [PATCH 17/18] fix impl of getListeners to correctly remove function --- packages/react-native-renderer/src/ReactNativeGetListeners.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index 5e8e5213f9b4c..a26fbd409211c 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -139,7 +139,7 @@ export default function getListeners( // Remove from the event listener once it's been called stateNode.canonical.removeEventListener_unstable( mangledImperativeRegistrationName, - listenerFnWrapper, + listenerObj.listener, listenerObj.capture, ); From 15fc32a2bc337339d904e2a5934f8c41e8a8f6fd Mon Sep 17 00:00:00 2001 From: Joshua Gross Date: Wed, 2 Mar 2022 10:23:16 -0800 Subject: [PATCH 18/18] pass all args in to event listeners --- packages/react-native-renderer/src/ReactNativeGetListeners.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js index a26fbd409211c..2ab198c504383 100644 --- a/packages/react-native-renderer/src/ReactNativeGetListeners.js +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -117,7 +117,7 @@ export default function getListeners( // all imperative event listeners in a function to unwrap the SyntheticEvent // and pass them an Event. // When this API is more stable and used more frequently, we can revisit. - const listenerFnWrapper = function(syntheticEvent) { + const listenerFnWrapper = function(syntheticEvent, ...args) { const eventInst = new CustomEvent(mangledImperativeRegistrationName, { detail: syntheticEvent.nativeEvent, }); @@ -127,7 +127,7 @@ export default function getListeners( // $FlowFixMe eventInst.setSyntheticEvent(syntheticEvent); - listenerObj.listener(eventInst); + listenerObj.listener(eventInst, ...args); }; // Only call once?