Skip to content

Commit 382a7cb

Browse files
rubennortefacebook-github-bot
authored andcommitted
Remove enableDirectEventsInEventTarget feature flag (#57528)
Summary: Cleans up the `enableDirectEventsInEventTarget` JS feature flag and inlines its usages as `true`. Direct events (those that neither bubble nor capture, such as `onLayout`) now always take the fast target-only dispatch path when EventTarget-based event dispatching is enabled, without a separate gate. - Removed the flag definition and regenerated the feature flags. - Inlined the flag in `dispatchNativeEvent` so `rnIsDirect` is derived directly from the event config. - Updated integration/benchmark tests: dropped the flag pragmas, made the fast-path test always run, and removed the now-dead legacy-path test. Changelog: [Internal] Reviewed By: huntie Differential Revision: D111695476
1 parent 0fd7369 commit 382a7cb

5 files changed

Lines changed: 46 additions & 111 deletions

File tree

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,17 +1014,6 @@ const definitions: FeatureFlagDefinitions = {
10141014
},
10151015
ossReleaseStage: 'none',
10161016
},
1017-
enableDirectEventsInEventTarget: {
1018-
defaultValue: false,
1019-
metadata: {
1020-
dateAdded: '2026-07-06',
1021-
description:
1022-
'When enabled (together with enableNativeEventTargetEventDispatching), direct events (those that neither bubble nor capture, such as onLayout) are dispatched only to the target node via a fast path that skips construction and traversal of the ancestor event path.',
1023-
expectedReleaseValue: true,
1024-
purpose: 'experimentation',
1025-
},
1026-
ossReleaseStage: 'none',
1027-
},
10281017
enableImperativeEvents: {
10291018
defaultValue: false,
10301019
metadata: {

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

Lines changed: 1 addition & 7 deletions
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<<ff39edecf287d3fe3638da2f71e6bd1c>>
7+
* @generated SignedSource<<2be1f76084eb85288987229421d7585f>>
88
* @flow strict
99
* @noformat
1010
*/
@@ -33,7 +33,6 @@ export type ReactNativeFeatureFlagsJsOnly = Readonly<{
3333
animatedForceNativeDriver: Getter<boolean>,
3434
animatedShouldSyncValueBeforeStartCallback: Getter<boolean>,
3535
deferFlatListFocusChangeRenderUpdate: Getter<boolean>,
36-
enableDirectEventsInEventTarget: Getter<boolean>,
3736
enableImperativeEvents: Getter<boolean>,
3837
enableNativeEventTargetEventDispatching: Getter<boolean>,
3938
externalElementInspectionEnabled: Getter<boolean>,
@@ -162,11 +161,6 @@ export const animatedShouldSyncValueBeforeStartCallback: Getter<boolean> = creat
162161
*/
163162
export const deferFlatListFocusChangeRenderUpdate: Getter<boolean> = createJavaScriptFlagGetter('deferFlatListFocusChangeRenderUpdate', false);
164163

165-
/**
166-
* When enabled (together with enableNativeEventTargetEventDispatching), direct events (those that neither bubble nor capture, such as onLayout) are dispatched only to the target node via a fast path that skips construction and traversal of the ancestor event path.
167-
*/
168-
export const enableDirectEventsInEventTarget: Getter<boolean> = createJavaScriptFlagGetter('enableDirectEventsInEventTarget', false);
169-
170164
/**
171165
* When enabled, ReactNativeElement and ReadOnlyText expose the public EventTarget API (addEventListener, removeEventListener, dispatchEvent). When disabled, those methods are removed from those final classes.
172166
*/

packages/react-native/src/private/renderer/core/__tests__/EventDispatching-benchmark-itest.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* LICENSE file in the root directory of this source tree.
66
*
77
* @fantom_flags enableNativeEventTargetEventDispatching:*
8-
* @fantom_flags enableDirectEventsInEventTarget:*
98
* @flow strict-local
109
* @format
1110
*/

packages/react-native/src/private/renderer/core/__tests__/EventTargetDispatching-itest.js

Lines changed: 40 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
*
77
* @fantom_flags enableNativeEventTargetEventDispatching:*
88
* @fantom_flags enableImperativeEvents:*
9-
* @fantom_flags enableDirectEventsInEventTarget:*
109
* @flow strict-local
1110
* @format
1211
*/
@@ -1730,94 +1729,51 @@ const {isOSS} = Fantom.getConstants();
17301729
).toBe(0);
17311730
});
17321731

1733-
(ReactNativeFeatureFlags.enableDirectEventsInEventTarget()
1734-
? it
1735-
: it.skip)(
1736-
'restricts the event path to the target (AT_TARGET only, no ancestor capture listeners)',
1737-
() => {
1738-
const root = Fantom.createRoot();
1739-
const childRef = React.createRef<React.ElementRef<typeof View>>();
1740-
const ancestorCapture = jest.fn();
1741-
let observedPhase: number | null = null;
1742-
let observedPathLength: number | null = null;
1743-
let observedCurrentIsTarget: boolean | null = null;
1744-
1745-
const handler = jest.fn((e: $FlowFixMe) => {
1746-
observedPhase = e.eventPhase;
1747-
observedPathLength = e.composedPath().length;
1748-
observedCurrentIsTarget = e.currentTarget === childRef.current;
1749-
});
1750-
1751-
Fantom.runTask(() => {
1752-
root.render(createNestedViewsWithLayout(5, childRef, () => {}));
1753-
});
1754-
1755-
asEventTarget(childRef.current).addEventListener('layout', handler);
1756-
asEventTarget(root.document.documentElement).addEventListener(
1757-
'layout',
1758-
ancestorCapture,
1759-
{capture: true},
1760-
);
1761-
Fantom.flushAllNativeEvents();
1762-
1763-
const ancestorCaptureBefore = ancestorCapture.mock.calls.length;
1764-
1765-
Fantom.dispatchNativeEvent(
1766-
childRef,
1767-
'onLayout',
1768-
{layout: {x: 0, y: 0, width: 100, height: 50}},
1769-
{category: Fantom.NativeEventCategory.Discrete},
1770-
);
1732+
it('restricts the event path to the target (AT_TARGET only, no ancestor capture listeners)', () => {
1733+
const root = Fantom.createRoot();
1734+
const childRef = React.createRef<React.ElementRef<typeof View>>();
1735+
const ancestorCapture = jest.fn();
1736+
let observedPhase: number | null = null;
1737+
let observedPathLength: number | null = null;
1738+
let observedCurrentIsTarget: boolean | null = null;
1739+
1740+
const handler = jest.fn((e: $FlowFixMe) => {
1741+
observedPhase = e.eventPhase;
1742+
observedPathLength = e.composedPath().length;
1743+
observedCurrentIsTarget = e.currentTarget === childRef.current;
1744+
});
17711745

1772-
expect(handler).toHaveBeenCalled();
1773-
expect(observedPhase).toBe(Event.AT_TARGET);
1774-
// Event path is just the target node.
1775-
expect(observedPathLength).toBe(1);
1776-
expect(observedCurrentIsTarget).toBe(true);
1777-
// With the fast path, the capture phase does not traverse ancestors.
1778-
expect(
1779-
ancestorCapture.mock.calls.length - ancestorCaptureBefore,
1780-
).toBe(0);
1781-
},
1782-
);
1746+
Fantom.runTask(() => {
1747+
root.render(createNestedViewsWithLayout(5, childRef, () => {}));
1748+
});
17831749

1784-
(ReactNativeFeatureFlags.enableDirectEventsInEventTarget()
1785-
? it.skip
1786-
: it)(
1787-
'without the fast path, the capture phase still traverses ancestors for direct events',
1788-
() => {
1789-
const root = Fantom.createRoot();
1790-
const childRef = React.createRef<React.ElementRef<typeof View>>();
1791-
const ancestorCapture = jest.fn();
1792-
1793-
Fantom.runTask(() => {
1794-
root.render(createNestedViewsWithLayout(5, childRef, () => {}));
1795-
});
1796-
1797-
asEventTarget(root.document.documentElement).addEventListener(
1798-
'layout',
1799-
ancestorCapture,
1800-
{capture: true},
1801-
);
1802-
Fantom.flushAllNativeEvents();
1750+
asEventTarget(childRef.current).addEventListener('layout', handler);
1751+
asEventTarget(root.document.documentElement).addEventListener(
1752+
'layout',
1753+
ancestorCapture,
1754+
{capture: true},
1755+
);
1756+
Fantom.flushAllNativeEvents();
18031757

1804-
const ancestorCaptureBefore = ancestorCapture.mock.calls.length;
1758+
const ancestorCaptureBefore = ancestorCapture.mock.calls.length;
18051759

1806-
Fantom.dispatchNativeEvent(
1807-
childRef,
1808-
'onLayout',
1809-
{layout: {x: 0, y: 0, width: 100, height: 50}},
1810-
{category: Fantom.NativeEventCategory.Discrete},
1811-
);
1760+
Fantom.dispatchNativeEvent(
1761+
childRef,
1762+
'onLayout',
1763+
{layout: {x: 0, y: 0, width: 100, height: 50}},
1764+
{category: Fantom.NativeEventCategory.Discrete},
1765+
);
18121766

1813-
// The DOM dispatch algorithm runs the capture phase over every
1814-
// ancestor even for non-bubbling events, so the ancestor capture
1815-
// listener fires.
1816-
expect(
1817-
ancestorCapture.mock.calls.length - ancestorCaptureBefore,
1818-
).toBe(1);
1819-
},
1820-
);
1767+
expect(handler).toHaveBeenCalled();
1768+
expect(observedPhase).toBe(Event.AT_TARGET);
1769+
// Event path is just the target node.
1770+
expect(observedPathLength).toBe(1);
1771+
expect(observedCurrentIsTarget).toBe(true);
1772+
// With the fast path, the capture phase does not traverse ancestors.
1773+
expect(ancestorCapture.mock.calls.length - ancestorCaptureBefore).toBe(
1774+
0,
1775+
);
1776+
});
18211777
});
18221778
},
18231779
);

packages/react-native/src/private/renderer/events/dispatchNativeEvent.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
customBubblingEventTypes,
1515
customDirectEventTypes,
1616
} from '../../../../Libraries/Renderer/shims/ReactNativeViewConfigRegistry';
17-
import * as ReactNativeFeatureFlags from '../../featureflags/ReactNativeFeatureFlags';
1817
import {
1918
setBubbledPropName,
2019
setCapturedPropName,
@@ -62,13 +61,11 @@ export default function dispatchNativeEvent(
6261
bubbleConfig.phasedRegistrationNames.skipBubbling !== true;
6362

6463
// A "direct" event is one registered only in the direct-event config
65-
// (e.g. `onLayout`): it neither bubbles nor captures. When the feature
66-
// flag is enabled, tag it so the EventTarget dispatch takes the fast
67-
// target-only path. Note that bubbling events with `skipBubbling` (e.g.
68-
// `onPointerEnter`) still have a capture phase and are NOT direct.
64+
// (e.g. `onLayout`): it neither bubbles nor captures. Tag it so the
65+
// EventTarget dispatch takes the fast target-only path. Note that
66+
// bubbling events with `skipBubbling` (e.g. `onPointerEnter`) still have
67+
// a capture phase and are NOT direct.
6968
const isDirect = bubbleConfig == null && directConfig != null;
70-
const rnIsDirect =
71-
isDirect && ReactNativeFeatureFlags.enableDirectEventsInEventTarget();
7269

7370
const eventType = topLevelTypeToEventType(type);
7471
const options: {
@@ -78,7 +75,7 @@ export default function dispatchNativeEvent(
7875
} = {
7976
bubbles,
8077
cancelable: true,
81-
rnIsDirect,
78+
rnIsDirect: isDirect,
8279
};
8380

8481
// Preserve the native event timestamp for backwards compatibility.

0 commit comments

Comments
 (0)