rive-app / rive-app/rive-react-native
[Android] Rive events dispatched off the UI thread can deadlock with Reanimated under Fabric (ANR)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 783
- Forks
- 81
- Avg merge
- 3d 6h
- Merged PRs (30d)
- 3
Description
Submission checklist
- I have confirmed the issue is present in the latest version of
rive-react-native(verified by source inspection of9.8.5andmain— see below) - I have searched the documentation and forums and could not find an answer
- I have searched existing issues and this is not a duplicate
Description
On Android, RiveReactNativeView dispatches all of its JS events from whatever thread the Rive runtime happens to call it on:
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
receiveEventsynchronously 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-reanimatedpresent 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in android/src/main/java/com/rivereactnative/RiveReactNativeView.kt at the event dispatch around lines 222-224, then inspect the other six dispatch sites named in the issue. Move each bridge delivery onto the UI thread as described, preserving the event payloads, and verify that all seven sites use the same approach.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin, react-native
- Domain
- mobile, performance
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100