Skip to content
68 changes: 41 additions & 27 deletions runtimes/react-native/data-binding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,49 +58,63 @@ import { Demos } from "/snippets/demos.jsx";
<Tab title="New Runtime (Recommended)">
Use the `useViewModelInstance` hook to create a view model instance. You can pass a `RiveFile`, `ViewModel`, or `RiveViewRef` as the source.

Pass `async: true` to opt in to asynchronous instance creation. The flag signals that your component handles the loading state: the hook returns `{ instance, isLoading, error }` and the instance is not available on the first render, so gate rendering on it. This prepares your code for the new runtime implementation, where creating an instance is an asynchronous operation.

```tsx
import { Text } from 'react-native';
import { useRiveFile, useViewModelInstance, RiveView } from '@rive-app/react-native';

const { riveFile } = useRiveFile(require('./my_file.riv'));
const { riveFile, error: fileError } = useRiveFile(require('./my_file.riv'));

// From RiveFile — default artboard's ViewModel, default instance
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

if (fileError || error) return <Text>{(fileError ?? error)!.message}</Text>;

return riveFile && instance && (
<RiveView file={riveFile} dataBind={instance} />
);
```

All lookup options combine with `async: true`:

```tsx
// Specify artboard or ViewModel name (mutually exclusive)
const { instance } = useViewModelInstance(riveFile, { artboardName: 'MainArtboard' });
const { instance } = useViewModelInstance(riveFile, { viewModelName: 'Settings' });
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true, artboardName: 'MainArtboard' });
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true, viewModelName: 'Settings' });

// instanceName can be combined with any of the above to pick a specific instance
const { instance } = useViewModelInstance(riveFile, { instanceName: 'PersonInstance' });
const { instance } = useViewModelInstance(riveFile, { viewModelName: 'Settings', instanceName: 'UserSettings' });
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true, instanceName: 'PersonInstance' });
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true, viewModelName: 'Settings', instanceName: 'UserSettings' });

// From a ViewModel object
const viewModel = riveFile?.viewModelByName('My View Model');
const { instance: namedInstance } = useViewModelInstance(viewModel, { name: 'My Instance' });
const { instance: newInstance } = useViewModelInstance(viewModel, { useNew: true });
const viewModel = await riveFile?.viewModelByNameAsync('My View Model');
const { instance: namedInstance, isLoading, error } = useViewModelInstance(viewModel, { async: true, name: 'My Instance' });
const { instance: newInstance, isLoading, error } = useViewModelInstance(viewModel, { async: true, useNew: true });

// With required: true (throws if null, use with Error Boundary)
const { instance } = useViewModelInstance(riveFile, { required: true });
// With required: true (throws once resolved to null, use with Error Boundary)
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true, required: true });

// With onInit to set initial values synchronously
const { instance } = useViewModelInstance(riveFile, {
// With onInit to set initial values before the instance is exposed or bound
const { instance, isLoading, error } = useViewModelInstance(riveFile, {
async: true,
onInit: (vmi) => {
vmi.numberProperty('health')?.set(100);
},
});

return (
<RiveView file={riveFile} dataBind={instance} />
);
```

You can also get the auto-bound instance from a `RiveViewRef`:
<Info>
`async: true` is available in `@rive-app/react-native@0.4.18` and later. The synchronous creation path is deprecated and will be removed in a future release.
</Info>

You can also get the auto-bound instance from a `RiveViewRef` — the hook waits for the view's auto-bound instance to become available:

```tsx
import { useRive, useViewModelInstance } from '@rive-app/react-native';

const { riveViewRef, setHybridRef } = useRive();
const { instance } = useViewModelInstance(riveViewRef);
const { instance, isLoading, error } = useViewModelInstance(riveViewRef, { async: true });
```

</Tab>
Expand Down Expand Up @@ -179,7 +193,7 @@ import { Demos } from "/snippets/demos.jsx";
import { RiveView, useRiveFile, useViewModelInstance } from '@rive-app/react-native';

const { riveFile } = useRiveFile(require('./my_file.riv'));
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

return (
<RiveView
Expand Down Expand Up @@ -227,7 +241,7 @@ import { Demos } from "/snippets/demos.jsx";
Or bind a specific instance:

```tsx
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });
<RiveView
file={riveFile}
dataBind={instance}
Expand Down Expand Up @@ -308,7 +322,7 @@ import { Demos } from "/snippets/demos.jsx";
} from '@rive-app/react-native';

const { riveFile } = useRiveFile(require('./my_file.riv'));
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

// Boolean
const { value: isActive, setValue: setIsActive, error: boolError } = useRiveBoolean(
Expand Down Expand Up @@ -442,7 +456,7 @@ import { Demos } from "/snippets/demos.jsx";
import { useRiveString, useRiveNumber, useRiveFile, useViewModelInstance } from '@rive-app/react-native';

const { riveFile } = useRiveFile(require('./my_file.riv'));
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

// Accessing 'settings/theme/name' (String)
const { value: themeName, setValue: setThemeName } = useRiveString(
Expand Down Expand Up @@ -504,7 +518,7 @@ import { Demos } from "/snippets/demos.jsx";
} from '@rive-app/react-native';

const { riveFile } = useRiveFile(require('./my_file.riv'));
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

const { value: boolValue, setValue: setBoolValue } = useRiveBoolean('My Boolean Property', instance);
const { value: stringValue, setValue: setStringValue } = useRiveString('My String Property', instance);
Expand Down Expand Up @@ -585,7 +599,7 @@ import { Demos } from "/snippets/demos.jsx";

const { riveViewRef, setHybridRef } = useRive();
const { riveFile } = useRiveFile(require('./my_file.riv'));
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });
const riveViewRef = useRef<RiveViewRef>(undefined);

const handleLoadImage = async () => {
Expand Down Expand Up @@ -665,7 +679,7 @@ import { Demos } from "/snippets/demos.jsx";
} from '@rive-app/react-native';

const { riveFile } = useRiveFile(require('./my_file.riv'));
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

// Get the list property with manipulation functions
const {
Expand Down Expand Up @@ -786,7 +800,7 @@ import { Demos } from "/snippets/demos.jsx";
} from '@rive-app/react-native';

const { riveFile } = useRiveFile(require('./my_file.riv'));
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

const { value: category, setValue: setCategory, error } = useRiveEnum(
'category',
Expand Down
66 changes: 63 additions & 3 deletions runtimes/react-native/migration-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,66 @@ title: "Migration Guide"
description: "Learn how to migrate your React Native app when upgrading between major versions of the Rive React Native runtime, including breaking changes and new features."
---

## Migrating to `v0.4.18`+

This release introduces async view model instance creation and deprecates the synchronous creation path.

### `useViewModelInstance` Gains `async: true` and `isLoading`

Pass `async: true` to opt in to asynchronous instance creation. The flag signals that your component handles the loading state: the result now includes `isLoading`, and the instance is not available on the first render — gate rendering on it. This prepares your code for the new runtime implementation, where creating an instance is an asynchronous operation.

`async: true` is a transitional flag; the synchronous creation path is being phased out in stages:

1. **Now** — calls without `async: true` are deprecated in TypeScript (deprecation warnings in your editor and via lint rules).
2. **Next** — calling without `async: true` will additionally log a runtime deprecation warning.
3. **Later** — the synchronous path is removed and `async: true` becomes required.
4. **Finally** — the flag itself is removed; the hook is simply asynchronous.

<Tabs>
<Tab title="New API">
```tsx
const { riveFile, error: fileError } = useRiveFile(require('./animation.riv'));
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

if (fileError || error) return <Text>{(fileError ?? error)!.message}</Text>;
if (isLoading || !instance) return <ActivityIndicator />;
return <RiveView file={riveFile} dataBind={instance} />;
```

</Tab>
<Tab title="Previous API (deprecated)">
```tsx
const { riveFile } = useRiveFile(require('./animation.riv'));
const { instance, error } = useViewModelInstance(riveFile);

if (error) return <Text>{error.message}</Text>;
if (!instance) return <ActivityIndicator />;
return <RiveView file={riveFile} dataBind={instance} />;
```

</Tab>
</Tabs>

`async` must stay constant for the lifetime of the component — remount (e.g. change `key`) to switch modes.

### `useRive` Ref Starts `undefined`

`useRive().riveViewRef` starts as `undefined` (view pending) instead of `null` (failed/detached), mirroring the `useRiveFile` convention. Code using `riveViewRef === null` as a "not ready yet" check should use `riveViewRef == null` or optional chaining instead.

### Other Changes

- On the synchronous (deprecated) path, a `null` source now settles to a terminal `{ instance: null, isLoading: false }` instead of reporting a loading state indefinitely.
- On Android, reading the auto-bound instance from a view ref right after mount can briefly resolve `null` while binding completes. The `async: true` path waits for the instance; one-shot reads via the deprecated sync path should be avoided.

### Quick Reference

| Previous | Replacement |
|---|---|
| `const { instance, error } = useViewModelInstance(file)` | `const { instance, isLoading, error } = useViewModelInstance(file, { async: true })` |
| `riveViewRef === null` (not-ready check) | `riveViewRef == null` |

---

## Migrating to `v0.4.0`+

This release improves error transparency and loading semantics for hooks, preparing for the async experimental runtime.
Expand Down Expand Up @@ -231,7 +291,7 @@ Synchronous `useMemo` chains for ViewModel setup should be replaced with `useSta
<Tab title="useViewModelInstance (Recommended)">
```tsx
const { riveFile } = useRiveFile(require('./animation.riv'));
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

const { value: health, setValue: setHealth } = useRiveNumber(
'health',
Expand Down Expand Up @@ -346,7 +406,7 @@ const [setRiveRef, riveRef] = useRive();
const [health, setHealth] = useRiveNumber(riveRef, "health");

// New: hooks take viewModelInstance, return objects
const { instance: viewModelInstance } = useViewModelInstance(riveFile);
const { instance: viewModelInstance, isLoading, error } = useViewModelInstance(riveFile, { async: true });
const { value: health, setValue: setHealth } = useRiveNumber(
"health",
viewModelInstance
Expand Down Expand Up @@ -554,7 +614,7 @@ Main API changes:
<Tab title="New Runtime">
```tsx
const { riveFile } = useRiveFile(require('./animation.riv'));
const { instance: viewModelInstance } = useViewModelInstance(riveFile);
const { instance: viewModelInstance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

const { value: health, setValue: setHealth } = useRiveNumber('health', viewModelInstance);
const { value: name, setValue: setName } = useRiveString('Player/Name', viewModelInstance);
Expand Down
45 changes: 27 additions & 18 deletions runtimes/react-native/react-native.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,14 @@ This guide documents how to get started using the Rive React Native runtime. The

This approach lets you set initial property values in the `onInit` callback before the view loads and decouples the `ViewModelInstance` from the `RiveView`.

```ts Manually create view model instance focus={6-8, 12, 18}
```ts Manually create view model instance focus={6-9, 13, 19}
export default function QuickStart() {
const { riveFile } = useRiveFile(
require('path/to/quick_start.riv')
);
const { riveViewRef, setHybridRef } = useRive();
const { instance: viewModelInstance } = useViewModelInstance(riveFile, {
const { instance: viewModelInstance, isLoading, error } = useViewModelInstance(riveFile, {
async: true,
onInit: (vmi) => vmi.numberProperty('health')!.set(20),
});

Expand All @@ -246,13 +247,14 @@ This guide documents how to get started using the Rive React Native runtime. The

Use the view model property hooks to update and listen to property changes.

```ts Property hooks focus={10-37, 50-52} expandable
```ts Property hooks focus={11-38, 51-53} expandable
export default function QuickStart() {
const { riveFile } = useRiveFile(
require('path/to/quick_start.riv')
);
const { riveViewRef, setHybridRef } = useRive();
const { instance: viewModelInstance } = useViewModelInstance(riveFile, {
const { instance: viewModelInstance, isLoading, error } = useViewModelInstance(riveFile, {
async: true,
onInit: (vmi) => vmi.numberProperty('health')!.set(20),
});

Expand Down Expand Up @@ -372,35 +374,42 @@ This guide documents how to get started using the Rive React Native runtime. The

### `useViewModelInstance`

Hook to create a view model instance from a `RiveFile`, `ViewModel`, or `RiveViewRef`:
Hook to create a view model instance from a `RiveFile`, `ViewModel`, or `RiveViewRef`.

Pass `async: true` to opt in to asynchronous instance creation — the flag signals that your component handles the loading state. The hook returns `{ instance, isLoading, error }` and the instance is not available on the first render. This prepares your code for the new runtime implementation, where creating an instance is an asynchronous operation.

```ts
// From RiveFile — default artboard's ViewModel, default instance
const { instance } = useViewModelInstance(riveFile);
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });

// From RiveFile — specify artboard or ViewModel name (mutually exclusive)
const { instance } = useViewModelInstance(riveFile, { artboardName: 'MainArtboard' });
const { instance } = useViewModelInstance(riveFile, { viewModelName: 'Settings' });
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true, artboardName: 'MainArtboard' });
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true, viewModelName: 'Settings' });

// instanceName can be combined with any of the above to pick a specific instance
const { instance } = useViewModelInstance(riveFile, { instanceName: 'PersonInstance' });
const { instance } = useViewModelInstance(riveFile, { viewModelName: 'Settings', instanceName: 'UserSettings' });
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true, instanceName: 'PersonInstance' });
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true, viewModelName: 'Settings', instanceName: 'UserSettings' });

// From a ViewModel object
const { instance: namedInstance } = useViewModelInstance(viewModel, { name: 'My Instance' });
const { instance: newInstance } = useViewModelInstance(viewModel, { useNew: true });
const { instance: namedInstance, isLoading, error } = useViewModelInstance(viewModel, { async: true, name: 'My Instance' });
const { instance: newInstance, isLoading, error } = useViewModelInstance(viewModel, { async: true, useNew: true });

// With required: true (throws if null, use with Error Boundary)
const { instance } = useViewModelInstance(riveFile, { required: true });
// With required: true (throws once resolved to null, use with Error Boundary)
const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true, required: true });

// With onInit to set initial values synchronously
const { instance } = useViewModelInstance(riveFile, {
// With onInit to set initial values before the instance is exposed or bound
const { instance, isLoading, error } = useViewModelInstance(riveFile, {
async: true,
onInit: (vmi) => {
vmi.numberProperty('health')!.set(100);
},
});
```

<Info>
`async: true` is available in `@rive-app/react-native@0.4.18` and later. The synchronous creation path is deprecated and will be removed in a future release.
</Info>

Pass the `dataBind` prop in `RiveView`.
```ts
return (
Expand All @@ -411,13 +420,13 @@ This guide documents how to get started using the Rive React Native runtime. The
);
```

You can also get the auto-bound instance from a `RiveViewRef`:
You can also get the auto-bound instance from a `RiveViewRef` — the hook waits for the view's auto-bound instance to become available:

```javascript
import { useRive, useViewModelInstance } from '@rive-app/react-native';

const { riveViewRef, setHybridRef } = useRive();
const { instance } = useViewModelInstance(riveViewRef);
const { instance, isLoading, error } = useViewModelInstance(riveViewRef, { async: true });
```

See the [runtime data binding documentation](/runtimes/react-native/data-binding) for more information.
Expand Down