Skip to content

Commit d49e0e0

Browse files
authored
Removed unused imperative events implementation from React Native renderer (#26282)
## Summary I'm going to start implementing parts of this proposal react-native-community/discussions-and-proposals#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.
1 parent 4111002 commit d49e0e0

10 files changed

+61
-368
lines changed

packages/react-native-renderer/src/ReactFabricEventEmitter.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ import {
2525
import {batchedUpdates} from './legacy-events/ReactGenericBatching';
2626
import accumulateInto from './legacy-events/accumulateInto';
2727

28-
import getListeners from './ReactNativeGetListeners';
28+
import getListener from './ReactNativeGetListener';
2929
import {runEventsInBatch} from './legacy-events/EventBatching';
3030

3131
import {RawEventEmitter} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
3232

33-
export {getListeners, registrationNameModules as registrationNames};
33+
export {getListener, registrationNameModules as registrationNames};
3434

3535
/**
3636
* Allows registered plugins an opportunity to extract events from top-level

packages/react-native-renderer/src/ReactFabricHostConfig.js

Lines changed: 0 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -99,28 +99,6 @@ export type RendererInspectionConfig = $ReadOnly<{
9999
) => void,
100100
}>;
101101

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-
124102
// TODO: Remove this conditional once all changes have propagated.
125103
if (registerEventHandler) {
126104
/**
@@ -137,7 +115,6 @@ class ReactFabricHostComponent {
137115
viewConfig: ViewConfig;
138116
currentProps: Props;
139117
_internalInstanceHandle: Object;
140-
_eventListeners: ?InternalEventListeners;
141118

142119
constructor(
143120
tag: number,
@@ -236,105 +213,6 @@ class ReactFabricHostComponent {
236213
setNativeProps(stateNode.node, updatePayload);
237214
}
238215
}
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-
}
338216
}
339217

340218
// $FlowFixMe[class-object-subtyping] found when upgrading Flow

packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@ import type {
1313
} from './legacy-events/PluginModuleType';
1414
import type {TopLevelType} from './legacy-events/TopLevelEventTypes';
1515
import SyntheticEvent from './legacy-events/SyntheticEvent';
16-
import type {PropagationPhases} from './legacy-events/PropagationPhases';
1716

1817
// Module provided by RN:
1918
import {ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
2019
import accumulateInto from './legacy-events/accumulateInto';
21-
import getListeners from './ReactNativeGetListeners';
20+
import getListener from './ReactNativeGetListener';
2221
import forEachAccumulated from './legacy-events/forEachAccumulated';
2322
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
24-
import isArray from 'shared/isArray';
2523

2624
const {customBubblingEventTypes, customDirectEventTypes} =
2725
ReactNativeViewConfigRegistry;
@@ -30,38 +28,10 @@ const {customBubblingEventTypes, customDirectEventTypes} =
3028
// EventPropagator.js, as they deviated from ReactDOM's newer
3129
// implementations.
3230
// $FlowFixMe[missing-local-annot]
33-
function listenersAtPhase(inst, event, propagationPhase: PropagationPhases) {
31+
function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
3432
const registrationName =
3533
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);
6535
}
6636

6737
// $FlowFixMe[missing-local-annot]
@@ -71,8 +41,14 @@ function accumulateDirectionalDispatches(inst, phase, event) {
7141
console.error('Dispatching inst must not be null');
7242
}
7343
}
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+
}
7652
}
7753

7854
// $FlowFixMe[missing-local-annot]
@@ -160,8 +136,14 @@ function accumulateDispatches(
160136
): void {
161137
if (inst && event && event.dispatchConfig.registrationName) {
162138
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+
}
165147
}
166148
}
167149

@@ -181,6 +163,7 @@ function accumulateDirectDispatches(events: ?(Array<Object> | Object)) {
181163
}
182164

183165
// End of inline
166+
type PropagationPhases = 'bubbled' | 'captured';
184167

185168
const ReactNativeBridgeEventPlugin = {
186169
eventTypes: ({}: EventTypes),

packages/react-native-renderer/src/ReactNativeEventEmitter.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ import {
2121
} from './legacy-events/EventPluginRegistry';
2222
import {batchedUpdates} from './legacy-events/ReactGenericBatching';
2323
import {runEventsInBatch} from './legacy-events/EventBatching';
24-
import getListeners from './ReactNativeGetListeners';
24+
import getListener from './ReactNativeGetListener';
2525
import accumulateInto from './legacy-events/accumulateInto';
2626

2727
import {getInstanceFromNode} from './ReactNativeComponentTree';
2828

29-
export {getListeners, registrationNameModules as registrationNames};
29+
export {getListener, registrationNameModules as registrationNames};
3030

3131
/**
3232
* Version of `ReactBrowserEventEmitter` that works on the receiving side of a
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)