[react-native-renderer] EventTarget-based event dispatching#36253
[react-native-renderer] EventTarget-based event dispatching#36253rubennorte merged 1 commit intofacebook:mainfrom
Conversation
0e1c577 to
52e1d67
Compare
| const {customBubblingEventTypes, customDirectEventTypes} = | ||
| ReactNativeViewConfigRegistry; |
There was a problem hiding this comment.
I think this usage is fine - but keep in mind these get lazily populated as view configs are accessed.
| // Process responder events before normal event dispatch. | ||
| // This handles touch negotiation (onStartShouldSetResponder, etc.) | ||
| processResponderEvent(topLevelType, targetFiber, nativeEvent); |
There was a problem hiding this comment.
Is this a change in ordering at all?
There was a problem hiding this comment.
No, this is the same logic we have now.
| export function topLevelTypeToEventName(topLevelType: string): string { | ||
| const fourthChar = topLevelType.charCodeAt(3); | ||
| if ( | ||
| topLevelType.startsWith('top') && | ||
| fourthChar >= 65 /* A */ && | ||
| fourthChar <= 90 /* Z */ | ||
| ) { | ||
| return topLevelType.slice(3).toLowerCase(); | ||
| } | ||
| return topLevelType; | ||
| } |
There was a problem hiding this comment.
We have this (and similar logic between onPress and topPress) in way too many places.
There was a problem hiding this comment.
We should just remove the top prefix from the native dispatch and then clean up all checks downstream.
| // Lazily built map from registration name (e.g. "onPointerUp", "onPointerUpCapture") | ||
| // to {eventName, capture}. Built from the view config registry on first access. | ||
| let registrationNameToEventConfig: { | ||
| [registrationName: string]: {eventName: string, capture: boolean}, |
There was a problem hiding this comment.
Probably a pre-existing issue, but what if different view configs define the same event name differently?
There was a problem hiding this comment.
Yeah, that's a pre-existing issue. I guess the first one would win.
| * | ||
| * Returns null if the prop name is not a registered event handler. | ||
| */ | ||
| export function propNameToEventConfig( |
There was a problem hiding this comment.
Yeah, I did a refactor after this and forgot to clean it up.
## Context
React Native's event system currently dispatches events through a legacy
plugin-based system. This change sets up an experiment to migrate to
the W3C EventTarget API (addEventListener/removeEventListener/dispatchEvent),
which enables imperative event handling on refs and aligns with web standards.
The experiment is gated behind the `enableNativeEventTargetEventDispatching`
feature flag (defaults to off).
## Changes
Event dispatching:
- Behind the flag, events are dispatched through `dispatchTrustedEvent`
instead of the legacy plugin system.
- `LegacySyntheticEvent` extends the W3C `Event` class and carries the
native event payload, providing backwards-compatible methods
(`persist`, `isDefaultPrevented`, `isPropagationStopped`).
- Native event timestamps are preserved using `setEventInitTimeStamp`,
matching the legacy behavior of reading `nativeEvent.timeStamp` or
`nativeEvent.timestamp`.
- `topLevelTypeToEventName` converts top-level types (e.g. "topPress")
to DOM event names (e.g. "press") for EventTarget dispatch.
Declarative (prop-based) event handlers:
- Props are NOT registered via addEventListener at commit time. Instead,
a hook on EventTarget (`EVENT_TARGET_GET_DECLARATIVE_LISTENER_KEY`)
extracts handlers from `canonical.currentProps` at dispatch time.
- `ReactNativeEventTypeMapping` provides `topLevelTypeToEventName` to
convert between the naming conventions.
Responder system:
- Responder events bypass EventTarget entirely. `negotiateResponder`
walks the fiber tree directly (capture then bubble), calling handlers
from `canonical.currentProps`. The first handler returning `true` wins.
- Lifecycle events call handlers directly and inspect return values:
`onResponderGrant` returning `true` blocks native responder,
`onResponderTerminationRequest` returning `false` refuses termination.
Type fixes:
- `AnyNativeEvent` corrected from `Event | KeyboardEvent | MouseEvent |
TouchEvent` to `{[string]: mixed}` to match the actual C++ native
event objects, removing several FlowFixMe suppressions.
- `dispatchEvent` entry point validates `nativeEvent` is a non-null
object, defaulting to `{}`.
- Flow libdef updated with `dispatchTrustedEvent` and
`setEventInitTimeStamp` declarations.
52e1d67 to
9b35af5
Compare
|
|
||
| // These globals are set by React Native (e.g. in setUpDOM.js, setUpTimers.js) | ||
| // and provide access to RN's feature flags. We use global functions because we | ||
| // don't have another mechanism to pass feature flags from RN to React in OSS. |
There was a problem hiding this comment.
Why isn't this using ReactFeatureFlags? We could hook that early in the init path to use the feature flag value.
There was a problem hiding this comment.
There's no easy way to hook that up ensuring that the flags are consistent, which is why I went for this option. If the function is defined, we know the flags will be consistent.
| @@ -0,0 +1,572 @@ | |||
| /** | |||
There was a problem hiding this comment.
Any changes here you want to highlight from the previous implementation in ResponderEventPlugin?
Any way we could share more of the logic or maintain history? There's a lot of complexity here which would be useful to understand the history of in the future.
There was a problem hiding this comment.
This should be a 1:1 port of the old implementation, so the old history should still apply. Maybe the comments should help understanding the complexity?
There was a problem hiding this comment.
There's no source control history as far as I can see
There was a problem hiding this comment.
I mean that going past this change they'll be able to see it in the other file if that was necessary. I don't think there's a clean way to preserve history here.
## Summary Set up the experiment to migrate event dispatching in the React Native renderer to be based on the native EventTarget API. Behind the `enableNativeEventTargetEventDispatching` flag, events are dispatched through `dispatchTrustedEvent` instead of the legacy plugin system. Regular event handler props are NOT registered via addEventListener at commit time. Instead, a hook on EventTarget (`EVENT_TARGET_GET_DECLARATIVE_LISTENER_KEY`) extracts handlers from `canonical.currentProps` at dispatch time, shifting cost from every render to only when events fire. The hook is overridden in ReactNativeElement to look up the prop name via a reverse mapping from event names (built lazily from the view config registry). Responder events bypass EventTarget entirely. `negotiateResponder` walks the fiber tree directly (capture then bubble phase), calling handlers from `canonical.currentProps` and checking return values inline. Lifecycle events (`responderGrant`, `responderMove`, etc.) call handlers directly from props and inspect return values — `onResponderGrant` returning `true` blocks native responder, `onResponderTerminationRequest` returning `false` refuses termination. This eliminates all commit-time cost for responder events (no wrappers, no addEventListener, no `responderWrappers` on canonical). ## How did you test this change? Flow Tested e2e in RN using Fantom tests (that will land after this). DiffTrain build for [5682442](5682442)
## Summary We found a bug in the logic in #36253 and we realized it's very inconvenient to iterate on the implementation when it's in this repository, as we're forced to then synchronize it to RN to test changes. This moves the entire implementation to RN for simplicity and also to simplify some clean ups in the future (like removing `top` prefixes from native event types). ## How did you test this change? The changes are gated. Will test e2e in RN.
## Summary We found a bug in the logic in #36253 and we realized it's very inconvenient to iterate on the implementation when it's in this repository, as we're forced to then synchronize it to RN to test changes. This moves the entire implementation to RN for simplicity and also to simplify some clean ups in the future (like removing `top` prefixes from native event types). ## How did you test this change? The changes are gated. Will test e2e in RN. DiffTrain build for [0418c8a](0418c8a)
Summary
Set up the experiment to migrate event dispatching in the React Native renderer to be based on the native EventTarget API.
Behind the
enableNativeEventTargetEventDispatchingflag, events are dispatched throughdispatchTrustedEventinstead of the legacy plugin system.Regular event handler props are NOT registered via addEventListener at commit time. Instead, a hook on EventTarget
(
EVENT_TARGET_GET_DECLARATIVE_LISTENER_KEY) extracts handlers fromcanonical.currentPropsat dispatch time, shifting cost from every render to only when events fire. The hook is overridden in ReactNativeElement to look up the prop name via a reverse mapping from event names (built lazily from the view config registry).Responder events bypass EventTarget entirely.
negotiateResponderwalks the fiber tree directly (capture then bubble phase), calling handlers fromcanonical.currentPropsand checking return values inline. Lifecycle events (responderGrant,responderMove, etc.) call handlers directly from props and inspect return values —onResponderGrantreturningtrueblocks native responder,onResponderTerminationRequestreturningfalserefuses termination. This eliminates all commit-time cost for responder events (no wrappers, no addEventListener, noresponderWrapperson canonical).How did you test this change?
Flow
Tested e2e in RN using Fantom tests (that will land after this).