Submission checklist
Description
On Android, RiveReactNativeView dispatches all of its JS events from whatever thread the Rive runtime happens to call it on:
https://github.com/rive-app/rive-react-native/blob/main/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt#L222-L224
reactContext.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, Events.PLAY.toString(), data)
Under the New Architecture this can deadlock, producing an ANR:
- Rive's render thread advances the state machine and emits a listener callback (
onPlay / onPause / onStop / onLoopEnd / onStateChanged / onRiveEventReceived / the error path) while holding the state-machine advance lock.
- That callback calls
receiveEvent synchronously on the render thread.
- With Reanimated in the tree, delivery is routed through the main thread.
- If the main thread is at that moment waiting on the Rive lock held in step 1, the two threads are deadlocked: classic lock-ordering inversion. The app stops responding and Android raises an ANR.
The fix is to hand the event to the UI thread instead of emitting it inline, so the emitting thread never holds the Rive lock across the bridge call:
UiThreadUtil.runOnUiThread {
reactContext.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, Events.PLAY.toString(), data)
}
This also matches the general expectation that RCTEventEmitter delivery is initiated from the UI thread rather than an arbitrary native worker thread.
I've opened #445 applying this to all seven dispatch sites.
Being upfront about the evidence: this is a structural threading bug, and what I have is the mechanism plus a fix that resolves it in our app — not a minimal public reproduction. We run 9.8.0 with exactly this change applied as a local patch, and the ANR stopped. I verified by reading 9.8.5 and current main that all seven call sites are unchanged and UiThreadUtil is not used anywhere in the repo, so the code path is still present. If you'd like, I'm happy to try to put together a standalone repro — it needs Fabric plus Reanimated plus a Rive state machine emitting events under load, so it wasn't quick to reduce.
I'm aware rive-react-native is now the legacy runtime and that @rive-app/react-native is the path forward. Filing this under the README's "medium term: address major concerns in this legacy package while supporting migration", since an ANR is fairly load-bearing for apps that haven't migrated yet.
Previous working version
Unknown — the dispatch has been on the calling thread for as long as we've used the library. The deadlock surfaced for us after enabling the New Architecture (Fabric).
Reproduction steps / code
No minimal reproduction available (see the note above). The conditions under which we hit it:
- New Architecture / Fabric enabled
react-native-reanimated present in the tree
- A Rive state machine that emits events (
onStateChanged / onRiveEventReceived) while animating
<Rive
resourceName="some_state_machine"
stateMachineName="State Machine 1"
autoplay
onStateChanged={(machine, state) => { /* ... */ }}
onRiveEventReceived={(event) => { /* ... */ }}
/>
rive-react-native version
9.8.0 (code path verified unchanged in 9.8.5 and main)
Platform
Android only
React Native version
0.86.2
Expo setup
Expo prebuild
Expo SDK version
57.0.16
Device
Not device-specific — this is a lock-ordering deadlock between Rive's render thread and the main thread, so it reproduces across Android hardware rather than on a particular device. I can supply specific device/OS breakdowns on request.
OS version
Various Android versions (not version-specific, for the same reason)
Additional context
- Reanimated is what makes this reachable in practice, since it affects how the event reaches the main thread. Apps without it may never see the deadlock even though the unsafe dispatch is still there.
- The change is mechanical: seven call sites, no behavioural change to event payloads or ordering guarantees beyond moving delivery onto the UI thread.
Submission checklist
rive-react-native(verified by source inspection of9.8.5andmain— see below)Description
On Android,
RiveReactNativeViewdispatches all of its JS events from whatever thread the Rive runtime happens to call it on:https://github.com/rive-app/rive-react-native/blob/main/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt#L222-L224
Under the New Architecture this can deadlock, producing an ANR:
onPlay/onPause/onStop/onLoopEnd/onStateChanged/onRiveEventReceived/ the error path) while holding the state-machine advance lock.receiveEventsynchronously on the render thread.The fix is to hand the event to the UI thread instead of emitting it inline, so the emitting thread never holds the Rive lock across the bridge call:
This also matches the general expectation that
RCTEventEmitterdelivery is initiated from the UI thread rather than an arbitrary native worker thread.I've opened #445 applying this to all seven dispatch sites.
Being upfront about the evidence: this is a structural threading bug, and what I have is the mechanism plus a fix that resolves it in our app — not a minimal public reproduction. We run
9.8.0with exactly this change applied as a local patch, and the ANR stopped. I verified by reading9.8.5and currentmainthat all seven call sites are unchanged andUiThreadUtilis not used anywhere in the repo, so the code path is still present. If you'd like, I'm happy to try to put together a standalone repro — it needs Fabric plus Reanimated plus a Rive state machine emitting events under load, so it wasn't quick to reduce.I'm aware
rive-react-nativeis now the legacy runtime and that@rive-app/react-nativeis the path forward. Filing this under the README's "medium term: address major concerns in this legacy package while supporting migration", since an ANR is fairly load-bearing for apps that haven't migrated yet.Previous working version
Unknown — the dispatch has been on the calling thread for as long as we've used the library. The deadlock surfaced for us after enabling the New Architecture (Fabric).
Reproduction steps / code
No minimal reproduction available (see the note above). The conditions under which we hit it:
react-native-reanimatedpresent in the treeonStateChanged/onRiveEventReceived) while animatingrive-react-nativeversion9.8.0 (code path verified unchanged in 9.8.5 and
main)Platform
Android only
React Native version
0.86.2
Expo setup
Expo prebuild
Expo SDK version
57.0.16
Device
Not device-specific — this is a lock-ordering deadlock between Rive's render thread and the main thread, so it reproduces across Android hardware rather than on a particular device. I can supply specific device/OS breakdowns on request.
OS version
Various Android versions (not version-specific, for the same reason)
Additional context