Skip to content

feat(hooks): async instance creation via useViewModelInstance({ async: true })#331

Merged
mfazekas merged 4 commits into
mainfrom
feat/use-viewmodel-instance-async-legacy
Jul 13, 2026
Merged

feat(hooks): async instance creation via useViewModelInstance({ async: true })#331
mfazekas merged 4 commits into
mainfrom
feat/use-viewmodel-instance-async-legacy

Conversation

@mfazekas

@mfazekas mfazekas commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Port of #304 (merged to feat/rive-ios-experimental) to main.

useViewModelInstance(source, { async: true }) creates the instance via the *Async runtime APIs off the JS thread and returns { instance, isLoading, error }. The sync overloads are deprecated per-overload (they block the JS thread via deprecated runtime APIs); async: true becomes the default in the next major. Overload/deprecation resolution is pinned with tsd (yarn typetest, gated in the CI lint job).

Native side: the *Async lookups now hop to the main thread (Android riveMainScope, iOS Promise.onMain) since the legacy runtime has no internal synchronization (the #297 race class), and Android's getViewModelInstance() returns a main-thread-maintained snapshot instead of traversing the controller off-thread, without blocking JS.

Porting notes:

  1. Experimental's legacy/ sources map onto this branch's flat layout (android/src/main, ios/); the new/-backend and expo57 harness changes from feat(hooks): async instance creation via useViewModelInstance({ async: true }) #304 don't apply here.
  2. src/hooks/useViewModelInstance.ts and the ported example files are taken from the experimental branch (re-run through main's prettier), which also brings a small pre-feat(hooks): async instance creation via useViewModelInstance({ async: true }) #304 improvement: createInstanceByName failures on the sync path now console.warn and resolve null instead of throwing raw native errors.
  3. The new e2e harness drops the android-experimental skips (RiveFileFactory.getBackend() doesn't exist here) and adds the arbtboards-models-instances.riv fixture.

Behavioral notes (worth a release-note callout):

  1. Android riveViewRef.getViewModelInstance() is now eventually consistent off the main thread: it returns the last main-thread snapshot and schedules a refresh, instead of the previous unsynchronized live read (the iOS Crash: Occasional crashes #297 race). One-shot readers — including the deprecated sync useViewModelInstance(riveViewRef) — can observe null where the racy read happened to catch a late auto-bind; the async: true path polls and is unaffected.
  2. useRive().riveViewRef now starts as undefined (view pending) instead of null (failed/detached), mirroring the useRiveFile convention, and its type widens to RiveViewRef | null | undefined.
  3. On the sync hook, a null source now settles to a terminal { instance: null, isLoading: false } instead of reporting undefined/loading forever.

Docs: rive-app/rive-docs#797 (draft until this ships).

mfazekas added 3 commits July 10, 2026 13:29
…: true })

Port of #304 from feat/rive-ios-experimental to main. The instance is
created via the *Async runtime APIs off the JS thread; the hook returns
{ instance, isLoading, error } and the sync overloads are deprecated
per-overload. Natives main-hop the *Async lookups (Android riveMainScope,
iOS Promise.onMain) and Android's getViewModelInstance() returns a
main-thread-maintained snapshot instead of traversing the controller
off-thread (issue #297 race class).
The ported comments compared "the new backend" to "the legacy backend",
which only reads on feat/rive-ios-experimental where both exist. Reword
so a reader of this branch knows the divergent behavior lives on the
experimental backend and why the hook still handles it here.
lastKnownViewModelInstance survived dispose, so a JS caller still holding
the view ref got the retained, already-released instance back (first
property access throws "Cannot acquire a disposed object") instead of
null. Clear it on the disposing detach, and resolve null from the
main-thread read once willDispose is set so a refresh posted before
dispose can't re-cache a released instance afterwards.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ports the “async ViewModelInstance creation” work onto main, adding an async: true mode to useViewModelInstance that uses the non-deprecated *Async runtime APIs to avoid blocking the JS thread, while deprecating the synchronous overloads. It also aligns null/undefined loading semantics across hooks and hardens native thread-safety around legacy runtime access (notably related to the #297 race class).

Changes:

  • Add useViewModelInstance(source, { async: true }) returning { instance, isLoading, error }, with per-overload deprecations for the sync path plus tsd pins and CI gating.
  • Update native iOS/Android *Async lookup paths to hop to the main thread; Android getViewModelInstance() becomes snapshot-based off-main.
  • Migrate example usage and add unit + e2e harness coverage for async behavior, overload resolution, and loading/null semantics.

Reviewed changes

Copilot reviewed 28 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
yarn.lock Adds tsd and dependency updates needed for type tests.
tsconfig.json Excludes **/*.test-d.ts from normal TS builds.
package.json Adds typetest script, tsd config, and ignores .test-d.ts in Jest.
.github/workflows/ci.yml Runs yarn typetest in CI.
eslint.config.mjs Ignores *.test-d.ts (owned by tsd) and .claude/ worktrees.
src/index.tsx Re-exports updated hook result types and adds required-result type export.
src/hooks/useViewModelInstanceAsync.ts New internal async implementation backing async: true behavior.
src/hooks/useViewModelInstance.ts Adds async?: boolean param, async dispatch + overload/deprecation strategy, and isLoading.
src/hooks/useRive.ts Changes riveViewRef initial state to undefined (pending) and widens the type.
src/hooks/tests/useViewModelInstance.test.ts Updates null/undefined source semantics assertions for sync path.
src/hooks/tests/useViewModelInstance.async.test.tsx Adds comprehensive unit tests for async mode behavior + invariants.
src/hooks/tests/useRive.test.ts Adds a unit test for riveViewRef being undefined before ready.
src/tests/useViewModelInstance.test-d.ts Adds tsd pins for overload/deprecation/required narrowing behavior.
ios/HybridRiveFile.swift Moves *Async lookups to Promise.onMain for legacy thread-safety.
android/src/main/java/com/margelo/nitro/rive/RiveMainScope.kt Adds a main-thread coroutine scope for legacy-safe async operations.
android/src/main/java/com/margelo/nitro/rive/HybridRiveFile.kt Hops *Async lookups onto main via riveMainScope.
android/src/main/java/com/margelo/nitro/rive/HybridViewModel.kt Hops *Async ViewModel APIs onto main via riveMainScope.
android/src/main/java/com/rive/RiveReactNativeView.kt Implements main-thread snapshotting for getViewModelInstance() and clears cache on detach.
android/src/main/java/com/margelo/nitro/rive/HybridRiveView.kt Documents thread-safety delegation to RiveReactNativeView.getViewModelInstance().
example/src/reproducers/Issue297ThreadRace.tsx Migrates to useViewModelInstance(..., { async: true }).
example/src/exercisers/RiveDataBindingExample.tsx Migrates to async: true.
example/src/exercisers/NestedViewModelExample.tsx Migrates to async: true.
example/src/exercisers/MenuListExample.tsx Migrates to async: true.
example/src/exercisers/FontFallbackExample.tsx Migrates to async: true.
example/src/demos/DataBindingArtboardsExample.tsx Migrates to async: true.
example/src/demos/QuickStart.tsx Migrates to async: true, adds error UI, and uses optional chaining for ref play.
example/src/tests/staleSetterWrite.test.tsx Adds explicit lint suppression to keep a sync-path regression test.
example/src/tests/issue297.hooks.test.tsx Adds explicit lint suppression to keep a sync-path #297 regression test.
example/tests/autoplay.harness.tsx Polls for auto-bound properties instead of reading immediately after ref attach.
example/tests/useViewModelInstance-async-e2e.harness.tsx Adds native-backed harness tests covering async hook contract across backends.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread android/src/main/java/com/rive/RiveReactNativeView.kt
Comment thread src/hooks/useViewModelInstanceAsync.ts
Comment thread src/hooks/useViewModelInstanceAsync.ts
The async hook's ref path polls getViewModelInstance() every 50ms;
allocating a Handler per off-main call was avoidable churn.

@HayesGordon HayesGordon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@mfazekas mfazekas merged commit 78052b5 into main Jul 13, 2026
25 of 27 checks passed
@mfazekas mfazekas deleted the feat/use-viewmodel-instance-async-legacy branch July 13, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants