Skip to content

Commit bec68ae

Browse files
rubennortefacebook-github-bot
authored andcommitted
Changelog: [Internal]
Summary: ## Context The current React Native Fabric renderer uses a legacy plugin-based event system (ResponderEventPlugin, ReactNativeBridgeEventPlugin, pooled SyntheticEvent objects) that duplicates dispatch logic already provided by the W3C EventTarget API. This adds complexity, prevents alignment with the Web platform, and blocks EventTarget-based features. ## Changes Behind the `enableNativeEventTargetEventDispatching` flag, native events are dispatched through `dispatchTrustedEvent` on the public instance tree instead of the legacy plugin system. Event handler props are extracted from `canonical.currentProps` at dispatch time via a declarative listener hook on EventTarget — no `addEventListener` registration at commit time. The responder system is fully self-contained: it walks the public instance tree directly for negotiation, calls handlers from props, and inspects return values inline (e.g. `onResponderGrant` returning `true` blocks native). It has no EventTarget coupling and no commit-time cost. Differential Revision: D100462547
1 parent dfddcc9 commit bec68ae

20 files changed

Lines changed: 3015 additions & 35 deletions

packages/react-native/Libraries/ReactNative/FabricUIManager.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ export interface Spec {
9292
/* width: */ number,
9393
/* height: */ number,
9494
];
95+
+setIsJSResponder: (
96+
node: Node | NativeElementReference,
97+
isJSResponder: boolean,
98+
blockNativeResponder: boolean,
99+
) => void;
95100
+unstable_DefaultEventPriority: number;
96101
+unstable_DiscreteEventPriority: number;
97102
+unstable_ContinuousEventPriority: number;
@@ -124,6 +129,7 @@ const CACHED_PROPERTIES = [
124129
'dispatchCommand',
125130
'compareDocumentPosition',
126131
'getBoundingClientRect',
132+
'setIsJSResponder',
127133
'unstable_DefaultEventPriority',
128134
'unstable_DiscreteEventPriority',
129135
'unstable_ContinuousEventPriority',

packages/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
* @format
99
*/
1010

11+
import typeof dispatchNativeEvent from '../../src/private/renderer/events/dispatchNativeEvent';
1112
import typeof CustomEvent from '../../src/private/webapis/dom/events/CustomEvent';
12-
import typeof {setEventInitTimeStamp} from '../../src/private/webapis/dom/events/internals/EventInternals';
13-
import typeof {dispatchTrustedEvent} from '../../src/private/webapis/dom/events/internals/EventTargetInternals';
1413
import typeof BatchedBridge from '../BatchedBridge/BatchedBridge';
1514
import typeof legacySendAccessibilityEvent from '../Components/AccessibilityInfo/legacySendAccessibilityEvent';
1615
import typeof TextInputState from '../Components/TextInput/TextInputState';
@@ -135,12 +134,8 @@ module.exports = {
135134
return require('../ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance')
136135
.getInternalInstanceHandleFromPublicInstance;
137136
},
138-
get dispatchTrustedEvent(): dispatchTrustedEvent {
139-
return require('../../src/private/webapis/dom/events/internals/EventTargetInternals')
140-
.dispatchTrustedEvent;
141-
},
142-
get setEventInitTimeStamp(): setEventInitTimeStamp {
143-
return require('../../src/private/webapis/dom/events/internals/EventInternals')
144-
.setEventInitTimeStamp;
137+
get dispatchNativeEvent(): dispatchNativeEvent {
138+
return require('../../src/private/renderer/events/dispatchNativeEvent')
139+
.default;
145140
},
146141
};

packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,17 @@ const definitions: FeatureFlagDefinitions = {
10431043
},
10441044
ossReleaseStage: 'none',
10451045
},
1046+
enableNativeEventTargetEventDispatching: {
1047+
defaultValue: false,
1048+
metadata: {
1049+
dateAdded: '2026-04-13',
1050+
description:
1051+
'When enabled, the React Native renderer dispatches events through the W3C EventTarget API (addEventListener/dispatchEvent) instead of the legacy plugin-based system.',
1052+
expectedReleaseValue: true,
1053+
purpose: 'experimentation',
1054+
},
1055+
ossReleaseStage: 'none',
1056+
},
10461057
externalElementInspectionEnabled: {
10471058
defaultValue: true,
10481059
metadata: {

packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<1ec2592998e830300fc777070dfdc49d>>
7+
* @generated SignedSource<<d1f406d9418791758ce4532e16a22f73>>
88
* @flow strict
99
* @noformat
1010
*/
@@ -33,6 +33,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{
3333
animatedShouldUseSingleOp: Getter<boolean>,
3434
deferFlatListFocusChangeRenderUpdate: Getter<boolean>,
3535
disableMaintainVisibleContentPosition: Getter<boolean>,
36+
enableNativeEventTargetEventDispatching: Getter<boolean>,
3637
externalElementInspectionEnabled: Getter<boolean>,
3738
fixVirtualizeListCollapseWindowSize: Getter<boolean>,
3839
isLayoutAnimationEnabled: Getter<boolean>,
@@ -162,6 +163,11 @@ export const deferFlatListFocusChangeRenderUpdate: Getter<boolean> = createJavaS
162163
*/
163164
export const disableMaintainVisibleContentPosition: Getter<boolean> = createJavaScriptFlagGetter('disableMaintainVisibleContentPosition', false);
164165

166+
/**
167+
* When enabled, the React Native renderer dispatches events through the W3C EventTarget API (addEventListener/dispatchEvent) instead of the legacy plugin-based system.
168+
*/
169+
export const enableNativeEventTargetEventDispatching: Getter<boolean> = createJavaScriptFlagGetter('enableNativeEventTargetEventDispatching', false);
170+
165171
/**
166172
* Enable the external inspection API for DevTools to communicate with the Inspector overlay.
167173
*/
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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+
* @fantom_flags enableNativeEventTargetEventDispatching:*
8+
* @flow strict-local
9+
* @format
10+
*/
11+
12+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
13+
14+
import * as Fantom from '@react-native/fantom';
15+
import * as React from 'react';
16+
import {View} from 'react-native';
17+
18+
let root: ReturnType<typeof Fantom.createRoot>;
19+
let ref: {current: React.ElementRef<typeof View> | null};
20+
21+
function createNestedViews(
22+
depth: number,
23+
innerRef: {current: React.ElementRef<typeof View> | null},
24+
): React.MixedElement {
25+
if (depth === 0) {
26+
return (
27+
<View
28+
ref={innerRef}
29+
collapsable={false}
30+
onPointerUp={() => {}}
31+
style={{width: 10, height: 10}}
32+
/>
33+
);
34+
}
35+
return (
36+
<View collapsable={false} onPointerUp={() => {}}>
37+
{createNestedViews(depth - 1, innerRef)}
38+
</View>
39+
);
40+
}
41+
42+
const {isOSS} = Fantom.getConstants();
43+
44+
if (isOSS) {
45+
it('is not supported in OSS yet', () => {
46+
expect(true).toBe(true);
47+
});
48+
} else {
49+
Fantom.unstable_benchmark
50+
.suite('Event Dispatching')
51+
.test(
52+
'dispatch event, flat (1 handler)',
53+
() => {
54+
Fantom.dispatchNativeEvent(
55+
ref,
56+
'onPointerUp',
57+
{x: 0, y: 0},
58+
{
59+
category: Fantom.NativeEventCategory.Discrete,
60+
},
61+
);
62+
},
63+
{
64+
beforeAll: () => {
65+
ref = React.createRef();
66+
},
67+
beforeEach: () => {
68+
root = Fantom.createRoot();
69+
Fantom.runTask(() => {
70+
root.render(
71+
<View
72+
ref={ref}
73+
collapsable={false}
74+
onPointerUp={() => {}}
75+
style={{width: 10, height: 10}}
76+
/>,
77+
);
78+
});
79+
},
80+
afterEach: () => {
81+
root.destroy();
82+
},
83+
},
84+
)
85+
.test(
86+
'dispatch event, nested 10 deep (bubbling)',
87+
() => {
88+
Fantom.dispatchNativeEvent(
89+
ref,
90+
'onPointerUp',
91+
{x: 0, y: 0},
92+
{
93+
category: Fantom.NativeEventCategory.Discrete,
94+
},
95+
);
96+
},
97+
{
98+
beforeAll: () => {
99+
ref = React.createRef();
100+
},
101+
beforeEach: () => {
102+
root = Fantom.createRoot();
103+
Fantom.runTask(() => {
104+
root.render(createNestedViews(10, ref));
105+
});
106+
},
107+
afterEach: () => {
108+
root.destroy();
109+
},
110+
},
111+
)
112+
.test(
113+
'dispatch event, nested 50 deep (bubbling)',
114+
() => {
115+
Fantom.dispatchNativeEvent(
116+
ref,
117+
'onPointerUp',
118+
{x: 0, y: 0},
119+
{
120+
category: Fantom.NativeEventCategory.Discrete,
121+
},
122+
);
123+
},
124+
{
125+
beforeAll: () => {
126+
ref = React.createRef();
127+
},
128+
beforeEach: () => {
129+
root = Fantom.createRoot();
130+
Fantom.runTask(() => {
131+
root.render(createNestedViews(50, ref));
132+
});
133+
},
134+
afterEach: () => {
135+
root.destroy();
136+
},
137+
},
138+
)
139+
.test(
140+
'dispatch event, nested 10 deep (no handlers on ancestors)',
141+
() => {
142+
Fantom.dispatchNativeEvent(
143+
ref,
144+
'onPointerUp',
145+
{x: 0, y: 0},
146+
{
147+
category: Fantom.NativeEventCategory.Discrete,
148+
},
149+
);
150+
},
151+
{
152+
beforeAll: () => {
153+
ref = React.createRef();
154+
},
155+
beforeEach: () => {
156+
root = Fantom.createRoot();
157+
Fantom.runTask(() => {
158+
let views: React.MixedElement = (
159+
<View
160+
ref={ref}
161+
collapsable={false}
162+
onPointerUp={() => {}}
163+
style={{width: 10, height: 10}}
164+
/>
165+
);
166+
for (let i = 0; i < 10; i++) {
167+
views = <View collapsable={false}>{views}</View>;
168+
}
169+
root.render(views);
170+
});
171+
},
172+
afterEach: () => {
173+
root.destroy();
174+
},
175+
},
176+
)
177+
.test(
178+
'dispatch event with stopPropagation, nested 10 deep',
179+
() => {
180+
Fantom.dispatchNativeEvent(
181+
ref,
182+
'onPointerUp',
183+
{x: 0, y: 0},
184+
{
185+
category: Fantom.NativeEventCategory.Discrete,
186+
},
187+
);
188+
},
189+
{
190+
beforeAll: () => {
191+
ref = React.createRef();
192+
},
193+
beforeEach: () => {
194+
root = Fantom.createRoot();
195+
Fantom.runTask(() => {
196+
let views: React.MixedElement = (
197+
<View
198+
ref={ref}
199+
collapsable={false}
200+
onPointerUp={e => {
201+
e.stopPropagation();
202+
}}
203+
style={{width: 10, height: 10}}
204+
/>
205+
);
206+
for (let i = 0; i < 10; i++) {
207+
views = (
208+
<View collapsable={false} onPointerUp={() => {}}>
209+
{views}
210+
</View>
211+
);
212+
}
213+
root.render(views);
214+
});
215+
},
216+
afterEach: () => {
217+
root.destroy();
218+
},
219+
},
220+
)
221+
.test(
222+
'render + dispatch, flat (handler update cost)',
223+
() => {
224+
Fantom.runTask(() => {
225+
root.render(
226+
<View
227+
ref={ref}
228+
collapsable={false}
229+
onPointerUp={() => {}}
230+
style={{width: 10, height: 10}}
231+
/>,
232+
);
233+
});
234+
Fantom.dispatchNativeEvent(
235+
ref,
236+
'onPointerUp',
237+
{x: 0, y: 0},
238+
{
239+
category: Fantom.NativeEventCategory.Discrete,
240+
},
241+
);
242+
},
243+
{
244+
beforeAll: () => {
245+
ref = React.createRef();
246+
},
247+
beforeEach: () => {
248+
root = Fantom.createRoot();
249+
Fantom.runTask(() => {
250+
root.render(
251+
<View
252+
ref={ref}
253+
collapsable={false}
254+
onPointerUp={() => {}}
255+
style={{width: 10, height: 10}}
256+
/>,
257+
);
258+
});
259+
},
260+
afterEach: () => {
261+
root.destroy();
262+
},
263+
},
264+
);
265+
}

0 commit comments

Comments
 (0)