From eb624e65165467cddadbd39ca5d413214308faf3 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:36:12 -0800 Subject: [PATCH 1/4] Generate fields to notify native if a JS event is being listened to --- .../generators/GenerateComponentWindows.ts | 27 ++++++++++- .../SampleCustomComponent/MovingLight.cpp | 37 ++++++++++----- .../SampleCustomComponent/CalendarView.g.h | 7 ++- .../SampleCustomComponent/ComboBox.g.h | 7 ++- .../CustomAccessibility.g.h | 1 + .../SampleCustomComponent/DrawingIsland.g.h | 1 + .../SampleCustomComponent/MovingLight.g.h | 19 +++++++- vnext/overrides.json | 6 +++ .../ViewConfigIgnore.windows.js | 46 +++++++++++++++++++ 9 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 vnext/src-win/Libraries/NativeComponent/ViewConfigIgnore.windows.js diff --git a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts index 36c2cd2e1e1..189c0b940d3 100644 --- a/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts +++ b/packages/@react-native-windows/codegen/src/generators/GenerateComponentWindows.ts @@ -342,6 +342,15 @@ export function createComponentGenerator({ }) .join('\n'); + const eventPropFields = componentShape.events.length + ? ` // These fields can be used to determine if JS has registered for this event\n` + + componentShape.events + .map(event => { + return ` REACT_FIELD(${event.name})\n bool ${event.name}{false};\n`; + }) + .join('\n') + : ''; + const propInitializers = componentShape.props .map(prop => { if (prop.typeAnnotation.type === 'MixedTypeAnnotation') @@ -350,6 +359,14 @@ export function createComponentGenerator({ }) .join('\n'); + const eventPropInitializers = componentShape.events.length + ? componentShape.events + .map(event => { + return ` ${event.name} = cloneFromProps->${event.name};`; + }) + .join('\n') + : ''; + const propObjectTypes = propObjectAliases.jobs .map(propObjectTypeName => { const propObjectType = propObjectAliases.types[propObjectTypeName]!; @@ -578,8 +595,14 @@ ${ .replace(/::_EVENT_EMITTER_NAME_::/g, eventEmitterName) .replace(/::_PROPS_NAME_::/g, propsName) .replace(/::_COMPONENT_NAME_::/g, componentName) - .replace(/::_PROP_INITIALIZERS_::/g, propInitializers) - .replace(/::_PROPS_FIELDS_::/g, propsFields) + .replace( + /::_PROP_INITIALIZERS_::/g, + [propInitializers, eventPropInitializers].join('\n'), + ) + .replace( + /::_PROPS_FIELDS_::/g, + [propsFields, eventPropFields].join('\n'), + ) .replace(/::_NAMESPACE_::/g, namespace) .replace(/\n\n\n+/g, '\n\n'); }; diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/MovingLight.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/MovingLight.cpp index ded0349f5fd..011d6a1cfe4 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/MovingLight.cpp +++ b/packages/sample-custom-component/windows/SampleCustomComponent/MovingLight.cpp @@ -29,6 +29,31 @@ struct MovingLight : public winrt::implements, m_eventParam = newProps->eventParam; } + if (!oldProps || oldProps->onSomething != newProps->onSomething) + { + if (newProps->onSomething) + { + m_pointerPressedRevoker = view.PointerPressed(winrt::auto_revoke, + [wkThis = get_weak()]( + const winrt::IInspectable& /*sender*/, + const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs& /*args*/) + { + if (auto strongThis = wkThis.get()) + { + if (auto eventEmitter = strongThis->EventEmitter()) + { + eventEmitter->onSomething( + { .value = strongThis->m_eventParam ? *strongThis->m_eventParam : "No eventParam set" }); + } + } + }); + } + else + { + m_pointerPressedRevoker.revoke(); + } + } + Codegen::BaseMovingLight::UpdateProps(sender, newProps, oldProps); } @@ -40,17 +65,6 @@ struct MovingLight : public winrt::implements, void Initialize(const winrt::Microsoft::ReactNative::ComponentView &sender) noexcept override { auto view = sender.as(); - view.PointerPressed( - [wkThis = get_weak()]( - const winrt::IInspectable & /*sender*/, - const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs & /*args*/) { - if (auto strongThis = wkThis.get()) { - if (auto eventEmitter = strongThis->EventEmitter()) { - eventEmitter->onSomething( - {.value = strongThis->m_eventParam ? *strongThis->m_eventParam : "No eventParam set"}); - } - } - }); } winrt::Microsoft::UI::Composition::Visual CreateVisual( @@ -92,6 +106,7 @@ struct MovingLight : public winrt::implements, std::optional m_eventParam; winrt::Microsoft::UI::Composition::SpriteVisual m_visual{nullptr}; winrt::Microsoft::UI::Composition::SpotLight m_spotlight{nullptr}; + winrt::Microsoft::ReactNative::Composition::ViewComponentView::PointerPressed_revoker m_pointerPressedRevoker; }; void RegisterMovingLightNativeComponent( diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h index edd1d5c8f1e..97936e547c7 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h +++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CalendarView.g.h @@ -25,7 +25,8 @@ struct CalendarViewProps : winrt::implements(); - label = cloneFromProps->label; + label = cloneFromProps->label; + onSelectedDatesChanged = cloneFromProps->onSelectedDatesChanged; } } @@ -36,6 +37,10 @@ struct CalendarViewProps : winrt::implements(); selectedIndex = cloneFromProps->selectedIndex; - placeholder = cloneFromProps->placeholder; + placeholder = cloneFromProps->placeholder; + onSelectionChanged = cloneFromProps->onSelectionChanged; } } @@ -40,6 +41,10 @@ struct ComboBoxProps : winrt::implements placeholder; + // These fields can be used to determine if JS has registered for this event + REACT_FIELD(onSelectionChanged) + bool onSelectionChanged{false}; + const winrt::Microsoft::ReactNative::ViewProps ViewProps; }; diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h index d372f4e7d32..78b3be8900e 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h +++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/CustomAccessibility.g.h @@ -25,6 +25,7 @@ struct CustomAccessibilityProps : winrt::implements(); + } } diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h index acb9244cef6..e0d24476147 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h +++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/DrawingIsland.g.h @@ -25,6 +25,7 @@ struct DrawingIslandProps : winrt::implements(); + } } diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h index b14125acc4c..7ad9144fd12 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h +++ b/packages/sample-custom-component/windows/SampleCustomComponent/codegen/react/components/SampleCustomComponent/MovingLight.g.h @@ -38,7 +38,11 @@ struct MovingLightProps : winrt::implementscolor; testMixed = cloneFromProps->testMixed.Copy(); eventParam = cloneFromProps->eventParam; - objectProp = cloneFromProps->objectProp; + objectProp = cloneFromProps->objectProp; + onSomething = cloneFromProps->onSomething; + onTestObjectEvent = cloneFromProps->onTestObjectEvent; + onEventWithInlineTypes = cloneFromProps->onEventWithInlineTypes; + onEventWithMultipleAliasTypes = cloneFromProps->onEventWithMultipleAliasTypes; } } @@ -61,6 +65,19 @@ struct MovingLightProps : winrt::implements objectProp; + // These fields can be used to determine if JS has registered for this event + REACT_FIELD(onSomething) + bool onSomething{false}; + + REACT_FIELD(onTestObjectEvent) + bool onTestObjectEvent{false}; + + REACT_FIELD(onEventWithInlineTypes) + bool onEventWithInlineTypes{false}; + + REACT_FIELD(onEventWithMultipleAliasTypes) + bool onEventWithMultipleAliasTypes{false}; + const winrt::Microsoft::ReactNative::ViewProps ViewProps; }; diff --git a/vnext/overrides.json b/vnext/overrides.json index bfe6a2804b9..a8de6e0f3ca 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -585,6 +585,12 @@ "baseFile": "packages/react-native/Libraries/NativeComponent/BaseViewConfig.ios.js", "baseHash": "a54d194cd731380ec0f2ff86b8b1f5ff072ec0ba" }, + { + "type": "derived", + "file": "src-win/Libraries/NativeComponent/ViewConfigIgnore.windows.js", + "baseFile": "packages/react-native/Libraries/NativeComponent/ViewConfigIgnore.js", + "baseHash": "ad955a66606d12afc8402e82afdd8103c5ca8b03" + }, { "type": "derived", "file": "src-win/Libraries/NativeModules/specs/NativeDialogManagerWindows.js", diff --git a/vnext/src-win/Libraries/NativeComponent/ViewConfigIgnore.windows.js b/vnext/src-win/Libraries/NativeComponent/ViewConfigIgnore.windows.js new file mode 100644 index 00000000000..19c28debe63 --- /dev/null +++ b/vnext/src-win/Libraries/NativeComponent/ViewConfigIgnore.windows.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +const ignoredViewConfigProps = new WeakSet < { ...} > (); + +/** + * Decorates ViewConfig values that are dynamically injected by the library, + * react-native-gesture-handler. (T45765076) + */ +export function DynamicallyInjectedByGestureHandler (object: T): T { + ignoredViewConfigProps.add(object); + return object; +} + +/** + * On iOS, ViewManager event declarations generate {eventName}: true entries + * in ViewConfig valueAttributes. These entries aren't generated for Android. + * This annotation allows Static ViewConfigs to insert these entries into + * iOS but not Android. + * + * In the future, we want to remove this platform-inconsistency. We want + * to set RN$ViewConfigEventValidAttributesDisabled = true server-side, + * so that iOS does not generate validAttributes from event props in iOS RCTViewManager, + * since Android does not generate validAttributes from events props in Android ViewManager. + * + * TODO(T110872225): Remove this logic, after achieving platform-consistency + */ +export function ConditionallyIgnoredEventHandlers< + const T: { +[name: string]: true }, +> (value: T): T | void { + return value; +} + +export function isIgnored(value: mixed): boolean { + if (typeof value === 'object' && value != null) { + return ignoredViewConfigProps.has(value); + } + return false; +} From 9fd90f0a889a2e91e291ba0ae73933654b04e44d Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:36:33 -0800 Subject: [PATCH 2/4] Change files --- ...ndows-codegen-e76cb014-87f2-4e71-af2d-1a489c5c60ca.json | 7 +++++++ ...ative-windows-45402c5d-6f84-4c62-930f-f77c7a9987cc.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 change/@react-native-windows-codegen-e76cb014-87f2-4e71-af2d-1a489c5c60ca.json create mode 100644 change/react-native-windows-45402c5d-6f84-4c62-930f-f77c7a9987cc.json diff --git a/change/@react-native-windows-codegen-e76cb014-87f2-4e71-af2d-1a489c5c60ca.json b/change/@react-native-windows-codegen-e76cb014-87f2-4e71-af2d-1a489c5c60ca.json new file mode 100644 index 00000000000..c187b305d1f --- /dev/null +++ b/change/@react-native-windows-codegen-e76cb014-87f2-4e71-af2d-1a489c5c60ca.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Generate fields to notify native if a JS event is being listened to", + "packageName": "@react-native-windows/codegen", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-45402c5d-6f84-4c62-930f-f77c7a9987cc.json b/change/react-native-windows-45402c5d-6f84-4c62-930f-f77c7a9987cc.json new file mode 100644 index 00000000000..437ce001ffb --- /dev/null +++ b/change/react-native-windows-45402c5d-6f84-4c62-930f-f77c7a9987cc.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Generate fields to notify native if a JS event is being listened to", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} From c1ed6a62bcdd0042943236b4600b1d0e576d0b5f Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:41:48 -0800 Subject: [PATCH 3/4] format --- .../SampleCustomComponent/MovingLight.cpp | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/packages/sample-custom-component/windows/SampleCustomComponent/MovingLight.cpp b/packages/sample-custom-component/windows/SampleCustomComponent/MovingLight.cpp index 011d6a1cfe4..e54b7bd38d6 100644 --- a/packages/sample-custom-component/windows/SampleCustomComponent/MovingLight.cpp +++ b/packages/sample-custom-component/windows/SampleCustomComponent/MovingLight.cpp @@ -29,29 +29,23 @@ struct MovingLight : public winrt::implements, m_eventParam = newProps->eventParam; } - if (!oldProps || oldProps->onSomething != newProps->onSomething) - { - if (newProps->onSomething) - { - m_pointerPressedRevoker = view.PointerPressed(winrt::auto_revoke, - [wkThis = get_weak()]( - const winrt::IInspectable& /*sender*/, - const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs& /*args*/) - { - if (auto strongThis = wkThis.get()) - { - if (auto eventEmitter = strongThis->EventEmitter()) - { - eventEmitter->onSomething( - { .value = strongThis->m_eventParam ? *strongThis->m_eventParam : "No eventParam set" }); + if (!oldProps || oldProps->onSomething != newProps->onSomething) { + if (newProps->onSomething) { + m_pointerPressedRevoker = view.PointerPressed( + winrt::auto_revoke, + [wkThis = get_weak()]( + const winrt::IInspectable & /*sender*/, + const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs & /*args*/) { + if (auto strongThis = wkThis.get()) { + if (auto eventEmitter = strongThis->EventEmitter()) { + eventEmitter->onSomething( + {.value = strongThis->m_eventParam ? *strongThis->m_eventParam : "No eventParam set"}); } - } - }); - } - else - { - m_pointerPressedRevoker.revoke(); - } + } + }); + } else { + m_pointerPressedRevoker.revoke(); + } } Codegen::BaseMovingLight::UpdateProps(sender, newProps, oldProps); From b93afc2369ed13e81af1a0a7ae7863d566a41910 Mon Sep 17 00:00:00 2001 From: Andrew Coates <30809111+acoates-ms@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:16:17 -0800 Subject: [PATCH 4/4] lint fix --- .../Libraries/NativeComponent/ViewConfigIgnore.windows.js | 1 - 1 file changed, 1 deletion(-) diff --git a/vnext/src-win/Libraries/NativeComponent/ViewConfigIgnore.windows.js b/vnext/src-win/Libraries/NativeComponent/ViewConfigIgnore.windows.js index 19c28debe63..f14c0a218fb 100644 --- a/vnext/src-win/Libraries/NativeComponent/ViewConfigIgnore.windows.js +++ b/vnext/src-win/Libraries/NativeComponent/ViewConfigIgnore.windows.js @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. * * @flow strict - * @format */ const ignoredViewConfigProps = new WeakSet < { ...} > ();