React Native throws Soft Exception when dispatching TouchEvents for custom native Android components
Description
A custom native Android component can be made up of multiple native Views. For example, a custom TabBar renders a BottomNavigationView which internally renders many native views for the images and text that make up each tab. These native views aren't created by React Native, they're created by Android. So React Native isn’t in charge of what ids get assigned to these internal views.
The ReactRootView dispatches TouchEvents using hit-testing to work out which View triggered the touch. When the ReactEventEmitter tries to find the default event emitter it uses the id of this target View to decide whether to use the new or old architecture emitter. An even number is the new arch and an odd number is the old. But if the target is one of these internal Views created by Android then it could be either even or odd and React Native will throw if it finds the wrong one.
This breaks on both the old and the new architecture. But I've labelled this as a new architecture bug because that's when this odd/even architecture indicator idea came in.
I could workaround this problem by setting all the descendants to have the same ids as the container. But this isn’t ideal and I'm hoping there's a React Native fix for this.
Steps to reproduce
- Download the new architecture sample
- Set the theme to be "Theme.Material3.DayNight.NoActionBar"
- Run the sample from Android Studio
- Click the tabs around the edge of the ripple
- Notice the soft exception logged
React Native Version
0.75.2
Affected Platforms
Runtime - Android
Areas
Fabric - The New Renderer
Output of npx react-native info
System:
OS: macOS 14.6.1
CPU: (8) arm64 Apple M1 Pro
Memory: 108.84 MB / 16.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 22.9.0
path: /opt/homebrew/bin/node
Yarn:
version: 3.6.4
path: /opt/homebrew/bin/yarn
npm:
version: 10.8.3
path: /opt/homebrew/bin/npm
Watchman:
version: 2024.10.07.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.15.2
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 24.0
- iOS 18.0
- macOS 15.0
- tvOS 18.0
- visionOS 2.0
- watchOS 11.0
Android SDK:
API Levels:
- "29"
- "30"
- "31"
- "32"
- "33"
- "34"
- "35"
Build Tools:
- 29.0.2
- 30.0.2
- 30.0.3
- 31.0.0
- 32.0.0
- 32.1.0
- 33.0.0
- 34.0.0
- 35.0.0
System Images:
- android-30 | Intel x86 Atom_64
- android-30 | Google APIs ARM 64 v8a
- android-30 | Google Play ARM 64 v8a
- android-32 | Google APIs ARM 64 v8a
- android-33 | Google APIs ARM 64 v8a
- android-33 | Google Play ARM 64 v8a
- android-34 | Google APIs ARM 64 v8a
- android-34 | Google Play ARM 64 v8a
- android-35 | Google APIs ARM 64 v8a
Android NDK: Not Found
IDEs:
Android Studio: 2024.2 AI-242.21829.142.2421.12409432
Xcode:
version: 16.0/16A242d
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.9
path: /usr/bin/javac
Ruby:
version: 3.3.5
path: /opt/homebrew/opt/ruby/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.3.1
wanted: 18.3.1
react-native:
installed: 0.75.2
wanted: 0.75.2
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: false
iOS:
hermesEnabled: true
newArchEnabled: true
Stacktrace or Logs
Unhandled SoftException
com.facebook.react.bridge.ReactNoCrashSoftException: Cannot find EventEmitter for receivedTouches: ReactTag[2131231014] UIManagerType[2] EventName[topTouchEnd]
at com.facebook.react.uimanager.events.ReactEventEmitter.receiveTouches(ReactEventEmitter.java:102)
at com.facebook.react.uimanager.events.TouchEvent.dispatchModern(TouchEvent.kt:124)
at com.facebook.react.uimanager.events.EventDispatcherImpl$DispatchEventsRunnable.run(EventDispatcherImpl.java:376)
at android.os.Handler.handleCallback(Handler.java:958)
at android.os.Handler.dispatchMessage(Handler.java:99)
at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:27)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:235)
at java.lang.Thread.run(Thread.java:1012)
Reproducer
https://github.com/grahammendick/navigation/tree/master/NavigationReactNative/sample/fabric
Screenshots and Videos
https://github.com/user-attachments/assets/a7d3608d-c23a-4830-99ae-665e4a2d6c56
| :warning: | Add or Reformat Version Info |
|---|---|
| :information_source: | We could not find or parse the version number of React Native in your issue report. Please use the template, and report your version including major, minor, and patch numbers - e.g. 0.70.2 |
| :warning: | Add or Reformat Version Info |
|---|---|
| :information_source: | We could not find or parse the version number of React Native in your issue report. Please use the template, and report your version including major, minor, and patch numbers - e.g. 0.70.2 |
I could workaround this problem by setting all the descendants to have the same ids as the container. But this isn’t ideal and I'm hoping there's a React Native fix for this.
Quick question: What is the problem?
Is the SoftException (that means a debug log)? If so, you're right we should fix it. However that's not super critical as it's not breaking the functionality of the app in any form
React Native is failing to raise TouchStart and TouchEnd events. I'm not sure if that's a problem. Maybe not for this BottomNavigationView example, but this will happen for any native Android component that's made up of multiple native Views. Also it's a SoftException now, but I'm concerned that React Native will change this to throw a hard exception in the future.
Hey @cortinico, is it something that should be supported by React Native? I mean managing nodes that React Native does not know about (they are not In shadow tree)? I am just thinking about the proper solution to this and where it should be located.
@coado The problem is that if a user taps a View that wasn't created by React Native then 50% of the time React Native won't be able to locate the EventEmitter. In these cases React Native fails to send TouchStart/End events and raises SoftExceptions.
React Native uses the tag of a View to decide which architecture it's on. This works for Views it creates because it uses odd numbers for one architecture and even numbers for the other. But it doesn't work for Views that it didn't create because then Android decides on the tag number.
The suggested fix is to remove this dependence on the View's tag number for determining the architecture.
Hey @grahammendick, just to clarify, is it an issue with old architecture only? The exception seems to not be visible on Fabric. You can see that in getUIManagerType, the uiManagerType is mainly determined by surfaceId, which should technically be always presented. I can see that Fabric wasn't enabled previously in the repro here.
@coado Oh, my bad, that's a great spot! I forgot to retest after I realised Fabric wasn't enabled. I've retested now and it doesn't happen on Fabric. I'm happy to close. Thank you 🙏