@samitouri / QOS-React-2 / commits / d49e0e0be0

Removed unused imperative events implementation from React Native renderer (#26282)

## Summary I'm going to start implementing parts of this proposal https://github.com/react-native-community/discussions-and-proposals/pull/607 As part of that implementation I'm going to refactor a few parts of the interface between React and React Native. One of the main problems we have right now is that we have private parts used by React and React Native in the public instance exported by refs. I want to properly separate that. I saw that a few methods to attach event handlers imperatively on refs were also exposing some things in the public instance (the `_eventListeners`). I checked and these methods are unused, so we can just clean them up instead of having to refactor them too. Adding support for imperative event listeners is in the roadmap after this proposal, and its implementation might differ after this refactor. This is essentially a manual revert of #23386. I'll submit more PRs after this for the rest of the refactor. ## How did you test this change? Existing jest tests. Will test a React sync internally at Meta.

Rubén Norte committed Mar 2, 2023 at 15:54 UTC d49e0e0be0941490fe709f80de137516ba4c0ee3
10 files changed +61 -368
packages/react-native-renderer/src/ReactFabricEventEmitter.js
+2 -2
@@ -25,12 +25,12 @@ import {
25 import {batchedUpdates} from './legacy-events/ReactGenericBatching';
26 import accumulateInto from './legacy-events/accumulateInto';
27
28 -import getListeners from './ReactNativeGetListeners';
28 +import getListener from './ReactNativeGetListener';
29 import {runEventsInBatch} from './legacy-events/EventBatching';
30
31 import {RawEventEmitter} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
32
33 -export {getListeners, registrationNameModules as registrationNames};
33 +export {getListener, registrationNameModules as registrationNames};
34
35 /**
36 * Allows registered plugins an opportunity to extract events from top-level
packages/react-native-renderer/src/ReactFabricHostConfig.js
-122
@@ -99,28 +99,6 @@ export type RendererInspectionConfig = $ReadOnly<{
99 ) => void,
100 }>;
101
102 -// TODO?: find a better place for this type to live
103 -export type EventListenerOptions = $ReadOnly<{
104 - capture?: boolean,
105 - once?: boolean,
106 - passive?: boolean,
107 - signal: mixed, // not yet implemented
108 -}>;
109 -export type EventListenerRemoveOptions = $ReadOnly<{
110 - capture?: boolean,
111 -}>;
112 -
113 -// TODO?: this will be changed in the future to be w3c-compatible and allow "EventListener" objects as well as functions.
114 -export type EventListener = Function;
115 -
116 -type InternalEventListeners = {
117 - [string]: {
118 - listener: EventListener,
119 - options: EventListenerOptions,
120 - invalidated: boolean,
121 - }[],
122 -};
123 -
102 // TODO: Remove this conditional once all changes have propagated.
103 if (registerEventHandler) {
104 /**
@@ -137,7 +115,6 @@ class ReactFabricHostComponent {
115 viewConfig: ViewConfig;
116 currentProps: Props;
117 _internalInstanceHandle: Object;
140 - _eventListeners: ?InternalEventListeners;
118
119 constructor(
120 tag: number,
@@ -236,105 +213,6 @@ class ReactFabricHostComponent {
213 setNativeProps(stateNode.node, updatePayload);
214 }
215 }
239 -
240 - // This API (addEventListener, removeEventListener) attempts to adhere to the
241 - // w3 Level2 Events spec as much as possible, treating HostComponent as a DOM node.
242 - //
243 - // Unless otherwise noted, these methods should "just work" and adhere to the W3 specs.
244 - // If they deviate in a way that is not explicitly noted here, you've found a bug!
245 - //
246 - // See:
247 - // * https://www.w3.org/TR/DOM-Level-2-Events/events.html
248 - // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
249 - // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
250 - //
251 - // And notably, not implemented (yet?):
252 - // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
253 - //
254 - //
255 - // Deviations from spec/TODOs:
256 - // (1) listener must currently be a function, we do not support EventListener objects yet.
257 - // (2) we do not support the `signal` option / AbortSignal yet
258 - addEventListener_unstable(
259 - eventType: string,
260 - listener: EventListener,
261 - options: EventListenerOptions | boolean,
262 - // $FlowFixMe[missing-local-annot]
263 - ) {
264 - if (typeof eventType !== 'string') {
265 - throw new Error('addEventListener_unstable eventType must be a string');
266 - }
267 - if (typeof listener !== 'function') {
268 - throw new Error('addEventListener_unstable listener must be a function');
269 - }
270 -
271 - // The third argument is either boolean indicating "captures" or an object.
272 - const optionsObj =
273 - typeof options === 'object' && options !== null ? options : {};
274 - const capture =
275 - (typeof options === 'boolean' ? options : optionsObj.capture) || false;
276 - const once = optionsObj.once || false;
277 - const passive = optionsObj.passive || false;
278 - const signal = null; // TODO: implement signal/AbortSignal
279 -
280 - /* $FlowFixMe the old version of Flow doesn't have a good way to define an
281 - * empty exact object. */
282 - const eventListeners: InternalEventListeners = this._eventListeners || {};
283 - if (this._eventListeners == null) {
284 - this._eventListeners = eventListeners;
285 - }
286 -
287 - const namedEventListeners = eventListeners[eventType] || [];
288 - if (eventListeners[eventType] == null) {
289 - eventListeners[eventType] = namedEventListeners;
290 - }
291 -
292 - namedEventListeners.push({
293 - listener: listener,
294 - invalidated: false,
295 - options: {
296 - capture: capture,
297 - once: once,
298 - passive: passive,
299 - signal: signal,
300 - },
301 - });
302 - }
303 -
304 - // See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
305 - removeEventListener_unstable(
306 - eventType: string,
307 - listener: EventListener,
308 - options: EventListenerRemoveOptions | boolean,
309 - ) {
310 - // eventType and listener must be referentially equal to be removed from the listeners
311 - // data structure, but in "options" we only check the `capture` flag, according to spec.
312 - // That means if you add the same function as a listener with capture set to true and false,
313 - // you must also call removeEventListener twice with capture set to true/false.
314 - const optionsObj =
315 - typeof options === 'object' && options !== null ? options : {};
316 - const capture =
317 - (typeof options === 'boolean' ? options : optionsObj.capture) || false;
318 -
319 - // If there are no event listeners or named event listeners, we can bail early - our
320 - // job is already done.
321 - const eventListeners = this._eventListeners;
322 - if (!eventListeners) {
323 - return;
324 - }
325 - const namedEventListeners = eventListeners[eventType];
326 - if (!namedEventListeners) {
327 - return;
328 - }
329 -
330 - // TODO: optimize this path to make remove cheaper
331 - eventListeners[eventType] = namedEventListeners.filter(listenerObj => {
332 - return !(
333 - listenerObj.listener === listener &&
334 - listenerObj.options.capture === capture
335 - );
336 - });
337 - }
216 }
217
218 // $FlowFixMe[class-object-subtyping] found when upgrading Flow
packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js
+20 -37
@@ -13,15 +13,13 @@ import type {
13 } from './legacy-events/PluginModuleType';
14 import type {TopLevelType} from './legacy-events/TopLevelEventTypes';
15 import SyntheticEvent from './legacy-events/SyntheticEvent';
16 -import type {PropagationPhases} from './legacy-events/PropagationPhases';
16
17 // Module provided by RN:
18 import {ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
19 import accumulateInto from './legacy-events/accumulateInto';
21 -import getListeners from './ReactNativeGetListeners';
20 +import getListener from './ReactNativeGetListener';
21 import forEachAccumulated from './legacy-events/forEachAccumulated';
22 import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
24 -import isArray from 'shared/isArray';
23
24 const {customBubblingEventTypes, customDirectEventTypes} =
25 ReactNativeViewConfigRegistry;
@@ -30,38 +28,10 @@ const {customBubblingEventTypes, customDirectEventTypes} =
28 // EventPropagator.js, as they deviated from ReactDOM's newer
29 // implementations.
30 // $FlowFixMe[missing-local-annot]
33 -function listenersAtPhase(inst, event, propagationPhase: PropagationPhases) {
31 +function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
32 const registrationName =
33 event.dispatchConfig.phasedRegistrationNames[propagationPhase];
36 - return getListeners(inst, registrationName, propagationPhase, true);
37 -}
38 -
39 -// $FlowFixMe[missing-local-annot]
40 -function accumulateListenersAndInstances(inst, event, listeners) {
41 - const listenersLength = listeners
42 - ? isArray(listeners)
43 - ? listeners.length
44 - : 1
45 - : 0;
46 - if (listenersLength > 0) {
47 - event._dispatchListeners = accumulateInto(
48 - event._dispatchListeners,
49 - listeners,
50 - );
51 -
52 - // Avoid allocating additional arrays here
53 - if (event._dispatchInstances == null && listenersLength === 1) {
54 - event._dispatchInstances = inst;
55 - } else {
56 - event._dispatchInstances = event._dispatchInstances || [];
57 - if (!isArray(event._dispatchInstances)) {
58 - event._dispatchInstances = [event._dispatchInstances];
59 - }
60 - for (let i = 0; i < listenersLength; i++) {
61 - event._dispatchInstances.push(inst);
62 - }
63 - }
64 - }
34 + return getListener(inst, registrationName);
35 }
36
37 // $FlowFixMe[missing-local-annot]
@@ -71,8 +41,14 @@ function accumulateDirectionalDispatches(inst, phase, event) {
41 console.error('Dispatching inst must not be null');
42 }
43 }
74 - const listeners = listenersAtPhase(inst, event, phase);
75 - accumulateListenersAndInstances(inst, event, listeners);
44 + const listener = listenerAtPhase(inst, event, phase);
45 + if (listener) {
46 + event._dispatchListeners = accumulateInto(
47 + event._dispatchListeners,
48 + listener,
49 + );
50 + event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
51 + }
52 }
53
54 // $FlowFixMe[missing-local-annot]
@@ -160,8 +136,14 @@ function accumulateDispatches(
136 ): void {
137 if (inst && event && event.dispatchConfig.registrationName) {
138 const registrationName = event.dispatchConfig.registrationName;
163 - const listeners = getListeners(inst, registrationName, 'bubbled', false);
164 - accumulateListenersAndInstances(inst, event, listeners);
139 + const listener = getListener(inst, registrationName);
140 + if (listener) {
141 + event._dispatchListeners = accumulateInto(
142 + event._dispatchListeners,
143 + listener,
144 + );
145 + event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
146 + }
147 }
148 }
149
@@ -181,6 +163,7 @@ function accumulateDirectDispatches(events: ?(Array<Object> | Object)) {
163 }
164
165 // End of inline
166 +type PropagationPhases = 'bubbled' | 'captured';
167
168 const ReactNativeBridgeEventPlugin = {
169 eventTypes: ({}: EventTypes),
packages/react-native-renderer/src/ReactNativeEventEmitter.js
+2 -2
@@ -21,12 +21,12 @@ import {
21 } from './legacy-events/EventPluginRegistry';
22 import {batchedUpdates} from './legacy-events/ReactGenericBatching';
23 import {runEventsInBatch} from './legacy-events/EventBatching';
24 -import getListeners from './ReactNativeGetListeners';
24 +import getListener from './ReactNativeGetListener';
25 import accumulateInto from './legacy-events/accumulateInto';
26
27 import {getInstanceFromNode} from './ReactNativeComponentTree';
28
29 -export {getListeners, registrationNameModules as registrationNames};
29 +export {getListener, registrationNameModules as registrationNames};
30
31 /**
32 * Version of `ReactBrowserEventEmitter` that works on the receiving side of a
packages/react-native-renderer/src/ReactNativeGetListener.js new
+37
@@ -0,0 +1,37 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11 +
12 +import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils';
13 +
14 +export default function getListener(
15 + inst: Fiber,
16 + registrationName: string,
17 +): Function | null {
18 + const stateNode = inst.stateNode;
19 + if (stateNode === null) {
20 + // Work in progress (ex: onload events in incremental mode).
21 + return null;
22 + }
23 + const props = getFiberCurrentPropsFromNode(stateNode);
24 + if (props === null) {
25 + // Work in progress.
26 + return null;
27 + }
28 + const listener = props[registrationName];
29 +
30 + if (listener && typeof listener !== 'function') {
31 + throw new Error(
32 + `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`,
33 + );
34 + }
35 +
36 + return listener;
37 +}
packages/react-native-renderer/src/ReactNativeGetListeners.js deleted
-166
@@ -1,166 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - * @flow
7 - */
8 -
9 -import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
10 -import type {PropagationPhases} from './legacy-events/PropagationPhases';
11 -
12 -import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils';
13 -import {CustomEvent} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
14 -
15 -/**
16 - * Get a list of listeners for a specific event, in-order.
17 - * For React Native we treat the props-based function handlers
18 - * as the first-class citizens, and they are always executed first
19 - * for both capture and bubbling phase.
20 - *
21 - * We need "phase" propagated to this point to support the HostComponent
22 - * EventEmitter API, which does not mutate the name of the handler based
23 - * on phase (whereas prop handlers are registered as `onMyEvent` and `onMyEvent_Capture`).
24 - *
25 - * Native system events emitted into React Native
26 - * will be emitted both to the prop handler function and to imperative event
27 - * listeners.
28 - *
29 - * This will either return null, a single Function without an array, or
30 - * an array of 2+ items.
31 - */
32 -export default function getListeners(
33 - inst: Fiber,
34 - registrationName: string,
35 - phase: PropagationPhases,
36 - dispatchToImperativeListeners: boolean,
37 -): null | Function | Array<Function> {
38 - const stateNode = inst.stateNode;
39 -
40 - if (stateNode === null) {
41 - return null;
42 - }
43 -
44 - // If null: Work in progress (ex: onload events in incremental mode).
45 - const props = getFiberCurrentPropsFromNode(stateNode);
46 - if (props === null) {
47 - // Work in progress.
48 - return null;
49 - }
50 -
51 - const listener = props[registrationName];
52 -
53 - if (listener && typeof listener !== 'function') {
54 - throw new Error(
55 - `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`,
56 - );
57 - }
58 -
59 - // If there are no imperative listeners, early exit.
60 - if (
61 - !(
62 - dispatchToImperativeListeners &&
63 - stateNode.canonical &&
64 - stateNode.canonical._eventListeners
65 - )
66 - ) {
67 - return listener;
68 - }
69 -
70 - // Below this is the de-optimized path.
71 - // If you are using _eventListeners, we do not (yet)
72 - // expect this to be as performant as the props-only path.
73 - // If/when this becomes a bottleneck, it can be refactored
74 - // to avoid unnecessary closures and array allocations.
75 - //
76 - // Previously, there was only one possible listener for an event:
77 - // the onEventName property in props.
78 - // Now, it is also possible to have N listeners
79 - // for a specific event on a node. Thus, we accumulate all of the listeners,
80 - // including the props listener, and return a function that calls them all in
81 - // order, starting with the handler prop and then the listeners in order.
82 - // We return either a non-empty array or null.
83 - const listeners = [];
84 - if (listener) {
85 - listeners.push(listener);
86 - }
87 -
88 - // TODO: for now, all of these events get an `rn:` prefix to enforce
89 - // that the user knows they're only getting non-W3C-compliant events
90 - // through this imperative event API.
91 - // Events might not necessarily be noncompliant, but we currently have
92 - // no verification that /any/ events are compliant.
93 - // Thus, we prefix to ensure no collision with W3C event names.
94 - const requestedPhaseIsCapture = phase === 'captured';
95 - const mangledImperativeRegistrationName = requestedPhaseIsCapture
96 - ? 'rn:' + registrationName.replace(/Capture$/, '')
97 - : 'rn:' + registrationName;
98 -
99 - // Get imperative event listeners for this event
100 - if (
101 - stateNode.canonical._eventListeners[mangledImperativeRegistrationName] &&
102 - stateNode.canonical._eventListeners[mangledImperativeRegistrationName]
103 - .length > 0
104 - ) {
105 - const eventListeners =
106 - stateNode.canonical._eventListeners[mangledImperativeRegistrationName];
107 -
108 - eventListeners.forEach(listenerObj => {
109 - // Make sure phase of listener matches requested phase
110 - const isCaptureEvent =
111 - listenerObj.options.capture != null && listenerObj.options.capture;
112 - if (isCaptureEvent !== requestedPhaseIsCapture) {
113 - return;
114 - }
115 -
116 - // For now (this is an area of future optimization) we must wrap
117 - // all imperative event listeners in a function to unwrap the SyntheticEvent
118 - // and pass them an Event.
119 - // When this API is more stable and used more frequently, we can revisit.
120 - // $FlowFixMe[missing-local-annot]
121 - const listenerFnWrapper = function (syntheticEvent, ...args) {
122 - const eventInst = new CustomEvent(mangledImperativeRegistrationName, {
123 - detail: syntheticEvent.nativeEvent,
124 - });
125 - eventInst.isTrusted = true;
126 - eventInst.setSyntheticEvent(syntheticEvent);
127 -
128 - listenerObj.listener(eventInst, ...args);
129 - };
130 -
131 - // Only call once?
132 - // If so, we ensure that it's only called once by setting a flag
133 - // and by removing it from eventListeners once it is called (but only
134 - // when it's actually been executed).
135 - if (listenerObj.options.once) {
136 - listeners.push(function (...args) {
137 - // Remove from the event listener once it's been called
138 - stateNode.canonical.removeEventListener_unstable(
139 - mangledImperativeRegistrationName,
140 - listenerObj.listener,
141 - listenerObj.capture,
142 - );
143 -
144 - // Guard against function being called more than once in
145 - // case there are somehow multiple in-flight references to
146 - // it being processed
147 - if (!listenerObj.invalidated) {
148 - listenerObj.invalidated = true;
149 - listenerObj.listener(...args);
150 - }
151 - });
152 - } else {
153 - listeners.push(listenerFnWrapper);
154 - }
155 - });
156 - }
157 -
158 - if (listeners.length === 0) {
159 - return null;
160 - }
161 - if (listeners.length === 1) {
162 - return listeners[0];
163 - }
164 -
165 - return listeners;
166 -}
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js deleted
-14
@@ -1,14 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -'use strict';
9 -
10 -// See the react-native repository for a full implementation.
11 -// This is just a stub, currently to pass `instanceof` checks.
12 -const CustomEvent = jest.fn();
13 -
14 -module.exports = {default: CustomEvent};
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js
-3
@@ -44,7 +44,4 @@ module.exports = {
44 get RawEventEmitter() {
45 return require('./RawEventEmitter').default;
46 },
47 - get CustomEvent() {
48 - return require('./CustomEvent').default;
49 - },
47 };
packages/react-native-renderer/src/legacy-events/PropagationPhases.js deleted
-10
@@ -1,10 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -export type PropagationPhases = 'bubbled' | 'captured';
scripts/flow/react-native-host-hooks.js
-12
@@ -143,18 +143,6 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
143 emit: (channel: string, event: RawEventEmitterEvent) => string,
144 ...
145 };
146 - declare export class CustomEvent {
147 - isTrusted: boolean;
148 -
149 - constructor(
150 - name: string,
151 - {
152 - detail: any,
153 - },
154 - ): void;
155 -
156 - setSyntheticEvent(event: any): void;
157 - }
146 }
147
148 declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' {