Skip to content

[Android] Rive events dispatched off the UI thread can deadlock with Reanimated under Fabric (ANR) #444

Description

@Togetic

Submission checklist

  • I have confirmed the issue is present in the latest version of rive-react-native (verified by source inspection of 9.8.5 and main — 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:

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:

  1. 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.
  2. That callback calls receiveEvent synchronously on the render thread.
  3. With Reanimated in the tree, delivery is routed through the main thread.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions