diff --git a/eslint.config.mjs b/eslint.config.mjs
index b891a1dd..447ee604 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,6 +1,8 @@
-import { fixupConfigRules } from '@eslint/compat';
+import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
+import tsPlugin from '@typescript-eslint/eslint-plugin';
+import tsParser from '@typescript-eslint/parser';
import prettier from 'eslint-plugin-prettier';
import { defineConfig } from 'eslint/config';
import path from 'node:path';
@@ -15,8 +17,8 @@ const compat = new FlatCompat({
});
export default defineConfig([
+ ...fixupConfigRules(compat.extends('@react-native', 'prettier')),
{
- extends: fixupConfigRules(compat.extends('@react-native', 'prettier')),
plugins: { prettier },
rules: {
'react/react-in-jsx-scope': 'off',
@@ -33,6 +35,20 @@ export default defineConfig([
],
},
},
+ {
+ files: ['src/**/*.{ts,tsx}', 'example/src/**/*.{ts,tsx}'],
+ plugins: { '@typescript-eslint': fixupPluginRules(tsPlugin) },
+ languageOptions: {
+ parser: tsParser,
+ parserOptions: {
+ projectService: true,
+ tsconfigRootDir: __dirname,
+ },
+ },
+ rules: {
+ '@typescript-eslint/no-deprecated': 'error',
+ },
+ },
{
ignores: [
'node_modules/',
diff --git a/example/__tests__/hooks.harness.tsx b/example/__tests__/hooks.harness.tsx
index b4bb234b..380cd785 100644
--- a/example/__tests__/hooks.harness.tsx
+++ b/example/__tests__/hooks.harness.tsx
@@ -146,6 +146,37 @@ describe('useRiveNumber Hook', () => {
cleanup();
});
+
+ it('delivers initial value on first listener emission without any prop update', async () => {
+ const file = await RiveFileFactory.fromSource(QUICK_START, undefined);
+ const vm = file.defaultArtboardViewModel();
+ expectDefined(vm);
+ const instance = vm.createDefaultInstance();
+ expectDefined(instance);
+
+ const context = createUseRiveNumberContext();
+
+ // Value must be undefined before mounting — no sneaky sync reads
+ expect(context.value).toBeUndefined();
+
+ await render(
+
+ );
+
+ // The listener fires its first emission on subscribe — no prop change required.
+ // Tight timeout: if this relied on a prop update cycle it would be flaky/fail.
+ await waitFor(
+ () => {
+ expect(typeof context.value).toBe('number');
+ },
+ { timeout: 1000 }
+ );
+
+ // Confirm no error occurred
+ expect(context.error).toBeNull();
+
+ cleanup();
+ });
});
describe('useViewModelInstance hook', () => {
diff --git a/example/src/demos/DataBindingArtboardsExample.tsx b/example/src/demos/DataBindingArtboardsExample.tsx
index 9af9b70b..3de225be 100644
--- a/example/src/demos/DataBindingArtboardsExample.tsx
+++ b/example/src/demos/DataBindingArtboardsExample.tsx
@@ -5,11 +5,12 @@ import {
ActivityIndicator,
Pressable,
} from 'react-native';
-import { useState, useMemo, useEffect, useRef } from 'react';
+import { useState, useEffect, useRef } from 'react';
import {
Fit,
RiveView,
useRiveFile,
+ useViewModelInstance,
type RiveFile,
type BindableArtboard,
} from '@rive-app/react-native';
@@ -77,16 +78,7 @@ function ArtboardSwapper({
mainFile: RiveFile;
assetsFile: RiveFile;
}) {
- // Get the view model from the "Main" artboard and create an instance
- // IMPORTANT: Must memoize to prevent creating new instance on every render
- const viewModel = useMemo(
- () => mainFile.defaultArtboardViewModel(),
- [mainFile]
- );
- const instance = useMemo(
- () => viewModel?.createDefaultInstance(),
- [viewModel]
- );
+ const instance = useViewModelInstance(mainFile);
const [currentArtboard, setCurrentArtboard] = useState('Dragon');
const initializedRef = useRef(false);
@@ -134,14 +126,10 @@ function ArtboardSwapper({
}
};
- if (!instance || !viewModel) {
+ if (!instance) {
return (
-
- {!viewModel
- ? 'No view model found in main file'
- : 'Failed to create instance'}
-
+
);
}
diff --git a/example/src/demos/QuickStart.tsx b/example/src/demos/QuickStart.tsx
index 95c2493e..4e167183 100644
--- a/example/src/demos/QuickStart.tsx
+++ b/example/src/demos/QuickStart.tsx
@@ -24,7 +24,7 @@ export default function QuickStart() {
);
const { riveViewRef, setHybridRef } = useRive();
const viewModelInstance = useViewModelInstance(riveFile, {
- onInit: (vmi) => (vmi.numberProperty('health')!.value = 9),
+ onInit: (vmi) => vmi.numberProperty('health')!.set(9),
});
const { setValue: setHealth } = useRiveNumber('health', viewModelInstance);
diff --git a/example/src/exercisers/FontFallbackExample.tsx b/example/src/exercisers/FontFallbackExample.tsx
index 84ecf81d..206f9c49 100644
--- a/example/src/exercisers/FontFallbackExample.tsx
+++ b/example/src/exercisers/FontFallbackExample.tsx
@@ -1,4 +1,4 @@
-import { useState, useMemo, useEffect } from 'react';
+import { useState, useEffect } from 'react';
import {
View,
Text,
@@ -15,6 +15,7 @@ import {
useRive,
useRiveFile,
useRiveString,
+ useViewModelInstance,
type FontSource,
type FallbackFont,
} from '@rive-app/react-native';
@@ -257,14 +258,7 @@ function MountedView({ text }: { text: string }) {
// https://rive.app/marketplace/26480-49641-simple-test-text-property/
require('../../assets/rive/font_fallback.riv')
);
- const viewModel = useMemo(
- () => riveFile?.defaultArtboardViewModel(),
- [riveFile]
- );
- const instance = useMemo(
- () => viewModel?.createDefaultInstance(),
- [viewModel]
- );
+ const instance = useViewModelInstance(riveFile ?? null);
const { setValue: setRiveText, error: textError } = useRiveString(
TEXT_PROPERTY,
@@ -280,7 +274,7 @@ function MountedView({ text }: { text: string }) {
riveViewRef?.playIfNeeded();
}, [text, setRiveText, riveViewRef]);
- if (isLoading) {
+ if (isLoading || !instance) {
return (
@@ -288,12 +282,10 @@ function MountedView({ text }: { text: string }) {
);
}
- if (error || !riveFile || !instance) {
+ if (error || !riveFile) {
return (
-
- {error || 'Failed to set up view model'}
-
+ {error || 'Failed to load file'}
);
}
diff --git a/example/src/exercisers/ManyViewModels.tsx b/example/src/exercisers/ManyViewModels.tsx
index 0fd994f5..8407f823 100644
--- a/example/src/exercisers/ManyViewModels.tsx
+++ b/example/src/exercisers/ManyViewModels.tsx
@@ -1,5 +1,5 @@
import { StyleSheet, View, Text, TouchableOpacity, Button } from 'react-native';
-import { useState, useMemo, useRef, useEffect } from 'react';
+import { useState, useRef, useEffect } from 'react';
import type { Metadata } from '../shared/metadata';
import {
DataBindMode,
@@ -84,17 +84,28 @@ export default function ManyViewModels() {
const riveViewRef = useRef(undefined);
const isListening = useRef(false);
- // Create a ViewModelInstance for "green" to demonstrate instance binding
- const greenInstance = useMemo(() => {
- if (!riveFile) return undefined;
- try {
- const viewModel = riveFile.defaultArtboardViewModel();
- if (!viewModel) return undefined;
- return viewModel.createInstanceByName('green');
- } catch (e) {
- console.error('Failed to create green instance:', e);
- return undefined;
+ const [greenInstance, setGreenInstance] = useState<
+ ViewModelInstance | undefined
+ >(undefined);
+
+ useEffect(() => {
+ if (!riveFile) return;
+ let cancelled = false;
+
+ async function setup() {
+ const viewModel = await riveFile!.defaultArtboardViewModelAsync();
+ if (cancelled || !viewModel) return;
+ const instance = await viewModel.createInstanceByNameAsync('green');
+ if (!cancelled) setGreenInstance(instance ?? undefined);
}
+
+ setup().catch((e) => {
+ if (!cancelled) console.error('Failed to create green instance:', e);
+ });
+
+ return () => {
+ cancelled = true;
+ };
}, [riveFile]);
const handleLoadImage = async () => {
diff --git a/example/src/exercisers/MenuListExample.tsx b/example/src/exercisers/MenuListExample.tsx
index c6cf5dfc..323bf473 100644
--- a/example/src/exercisers/MenuListExample.tsx
+++ b/example/src/exercisers/MenuListExample.tsx
@@ -7,10 +7,11 @@ import {
TextInput,
ScrollView,
} from 'react-native';
-import { useRef, useMemo } from 'react';
+import { useRef, useState, useEffect } from 'react';
import {
Fit,
RiveView,
+ type ViewModel,
type ViewModelInstance,
type RiveFile,
useRiveFile,
@@ -79,15 +80,27 @@ function MenuListContent({
error,
} = useRiveList('menu', instance);
- const listItemViewModel = useMemo(
- () => file.viewModelByName('listItem'),
- [file]
- );
+ const [listItemViewModel, setListItemViewModel] = useState<
+ ViewModel | undefined
+ >(undefined);
+
+ useEffect(() => {
+ let cancelled = false;
+ file
+ .viewModelByNameAsync('listItem')
+ .then((vm) => {
+ if (!cancelled) setListItemViewModel(vm ?? undefined);
+ })
+ .catch((e) => console.error('Failed to load listItem view model:', e));
+ return () => {
+ cancelled = true;
+ };
+ }, [file]);
- const addNewMenuItem = (label: string) => {
+ const addNewMenuItem = async (label: string) => {
if (!listItemViewModel) return;
- const newMenuItemVmi = listItemViewModel.createInstance();
+ const newMenuItemVmi = await listItemViewModel.createBlankInstanceAsync();
if (!newMenuItemVmi) return;
const labelProperty = newMenuItemVmi.stringProperty('label');
@@ -96,9 +109,9 @@ function MenuListContent({
if (!labelProperty || !hoverColorProperty || !fontIconProperty) return;
- labelProperty.value = label;
- hoverColorProperty.value = 0xff323232;
- fontIconProperty.value = '';
+ labelProperty.set(label);
+ hoverColorProperty.set(0xff323232);
+ fontIconProperty.set('');
lastAdded.current = newMenuItemVmi;
addInstance(newMenuItemVmi);
@@ -126,7 +139,7 @@ function MenuListContent({
const menuItemLabel = menuItem.stringProperty('label');
if (!menuItemLabel) return;
- menuItemLabel.value = label;
+ menuItemLabel.set(label);
};
return (
diff --git a/example/src/exercisers/NestedViewModelExample.tsx b/example/src/exercisers/NestedViewModelExample.tsx
index c90ef875..efa997b4 100644
--- a/example/src/exercisers/NestedViewModelExample.tsx
+++ b/example/src/exercisers/NestedViewModelExample.tsx
@@ -6,12 +6,13 @@ import {
Button,
TextInput,
} from 'react-native';
-import { useMemo, useRef, useState } from 'react';
+import { useRef, useState } from 'react';
import {
Fit,
RiveView,
useRiveFile,
useRiveString,
+ useViewModelInstance,
type ViewModelInstance,
type RiveFile,
type RiveViewRef,
@@ -37,20 +38,10 @@ export default function NestedViewModelExample() {
}
function WithViewModelSetup({ file }: { file: RiveFile }) {
- const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]);
- const instance = useMemo(
- () => viewModel?.createDefaultInstance(),
- [viewModel]
- );
+ const instance = useViewModelInstance(file);
- if (!instance || !viewModel) {
- return (
-
- {!viewModel
- ? 'No view model found'
- : 'Failed to create view model instance'}
-
- );
+ if (!instance) {
+ return ;
}
return ;
@@ -88,11 +79,10 @@ function ReplaceViewModelTest({
const addLog = (msg: string) => setLog((prev) => [...prev, msg]);
- const handleReplace = () => {
- // Get vm2's instance
- const vm2Instance = instance.viewModel('vm2');
+ const handleReplace = async () => {
+ const vm2Instance = await instance.viewModelAsync('vm2');
if (!vm2Instance) {
- addLog('❌ viewModel("vm2") returned undefined');
+ addLog('❌ viewModelAsync("vm2") returned undefined');
return;
}
addLog(`✅ Got vm2 instance: ${vm2Instance.instanceName}`);
diff --git a/example/src/exercisers/RiveDataBindingExample.tsx b/example/src/exercisers/RiveDataBindingExample.tsx
index d700eecb..42b0b1a2 100644
--- a/example/src/exercisers/RiveDataBindingExample.tsx
+++ b/example/src/exercisers/RiveDataBindingExample.tsx
@@ -1,9 +1,10 @@
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
-import { useEffect, useMemo } from 'react';
+import { useEffect } from 'react';
import {
Fit,
RiveView,
useRiveNumber,
+ useViewModelInstance,
type ViewModelInstance,
type RiveFile,
useRiveString,
@@ -34,20 +35,10 @@ export default function WithRiveFile() {
}
function WithViewModelSetup({ file }: { file: RiveFile }) {
- const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]);
- const instance = useMemo(
- () => viewModel?.createDefaultInstance(),
- [viewModel]
- );
+ const instance = useViewModelInstance(file);
- if (!instance || !viewModel) {
- return (
-
- {!viewModel
- ? 'No view model found'
- : 'Failed to create view model instance'}
-
- );
+ if (!instance) {
+ return ;
}
return ;
diff --git a/example/src/exercisers/RiveDataBindingExampleExpApi.tsx b/example/src/exercisers/RiveDataBindingExampleExpApi.tsx
new file mode 100644
index 00000000..8f4c2704
--- /dev/null
+++ b/example/src/exercisers/RiveDataBindingExampleExpApi.tsx
@@ -0,0 +1,163 @@
+import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
+import { useEffect, useState } from 'react';
+import {
+ Fit,
+ RiveView,
+ useRiveNumber,
+ type ViewModelInstance,
+ type RiveFile,
+ useRiveString,
+ useRiveColor,
+ useRiveTrigger,
+ useRiveFile,
+} from '@rive-app/react-native';
+import { type Metadata } from '../shared/metadata';
+
+export default function WithRiveFile() {
+ const { riveFile, isLoading, error } = useRiveFile(
+ require('../../assets/rive/rewards.riv')
+ );
+
+ return (
+
+
+ {isLoading ? (
+
+ ) : riveFile ? (
+
+ ) : (
+ {error || 'Unexpected error'}
+ )}
+
+
+ );
+}
+
+function WithViewModelSetup({ file }: { file: RiveFile }) {
+ const [instance, setInstance] = useState(
+ undefined
+ );
+ const [setupError, setSetupError] = useState(undefined);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function setup() {
+ const viewModel = await file.defaultArtboardViewModelAsync();
+ if (cancelled) return;
+
+ if (!viewModel) {
+ setSetupError('No view model found');
+ return;
+ }
+
+ const vmi = await viewModel.createDefaultInstanceAsync();
+ if (cancelled) return;
+
+ if (!vmi) {
+ setSetupError('Failed to create view model instance');
+ return;
+ }
+
+ setInstance(vmi);
+ }
+
+ setup().catch((e: unknown) => {
+ if (!cancelled) {
+ setSetupError(String(e));
+ }
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [file]);
+
+ if (setupError) {
+ return {setupError};
+ }
+
+ if (!instance) {
+ return ;
+ }
+
+ return ;
+}
+
+function DataBindingExample({
+ instance,
+ file,
+}: {
+ instance: ViewModelInstance;
+ file: RiveFile;
+}) {
+ const { error: coinValueError } = useRiveNumber('Coin/Item_Value', instance);
+
+ if (coinValueError) {
+ console.error('coinValueError', coinValueError);
+ }
+
+ const { setValue: setButtonText } = useRiveString('Button/State_1', instance);
+
+ const { setValue: setBarColor, error: barColorError } = useRiveColor(
+ 'Energy_Bar/Bar_Color',
+ instance
+ );
+
+ if (barColorError) {
+ console.error('barColorError', barColorError);
+ }
+
+ const { error: triggerError } = useRiveTrigger('Button/Pressed', instance, {
+ onTrigger: () => {
+ console.log('Button pressed');
+ },
+ });
+
+ if (triggerError) {
+ console.error('triggerError', triggerError);
+ }
+
+ useEffect(() => {
+ setButtonText("Let's go!");
+ setBarColor('#0000FF');
+ }, [setBarColor, setButtonText]);
+
+ return (
+
+ );
+}
+
+WithRiveFile.metadata = {
+ name: 'Data Binding (expapi)',
+ description:
+ 'Same as Data Binding but uses the async API (defaultArtboardViewModelAsync / createDefaultInstanceAsync)',
+} satisfies Metadata;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#fff',
+ },
+ riveContainer: {
+ flex: 1,
+ backgroundColor: '#f5f5f5',
+ },
+ rive: {
+ flex: 1,
+ width: '100%',
+ height: '100%',
+ },
+ errorText: {
+ color: 'red',
+ textAlign: 'center',
+ padding: 20,
+ },
+});
diff --git a/example/src/exercisers/RiveEventsExample.tsx b/example/src/exercisers/RiveEventsExample.tsx
index 413ef290..e6eec0d8 100644
--- a/example/src/exercisers/RiveEventsExample.tsx
+++ b/example/src/exercisers/RiveEventsExample.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-deprecated */
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { useState, useEffect } from 'react';
import {
diff --git a/example/src/exercisers/RiveStateMachineInputsExample.tsx b/example/src/exercisers/RiveStateMachineInputsExample.tsx
index 4a0ae8ee..6ec37820 100644
--- a/example/src/exercisers/RiveStateMachineInputsExample.tsx
+++ b/example/src/exercisers/RiveStateMachineInputsExample.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-deprecated */
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { useEffect } from 'react';
import { Fit, RiveView, useRive, useRiveFile } from '@rive-app/react-native';
diff --git a/example/src/exercisers/RiveTextRunExample.tsx b/example/src/exercisers/RiveTextRunExample.tsx
index d0c98f8a..19f7fa44 100644
--- a/example/src/exercisers/RiveTextRunExample.tsx
+++ b/example/src/exercisers/RiveTextRunExample.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-deprecated */
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { useEffect } from 'react';
import { Fit, RiveView, useRive, useRiveFile } from '@rive-app/react-native';
diff --git a/src/hooks/__tests__/useViewModelInstance.test.ts b/src/hooks/__tests__/useViewModelInstance.test.ts
index 09a70cd6..1e6382b2 100644
--- a/src/hooks/__tests__/useViewModelInstance.test.ts
+++ b/src/hooks/__tests__/useViewModelInstance.test.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-deprecated */
import { renderHook } from '@testing-library/react-native';
import { useViewModelInstance } from '../useViewModelInstance';
import type { RiveFile } from '../../specs/RiveFile.nitro';
diff --git a/src/hooks/useRiveList.ts b/src/hooks/useRiveList.ts
index 71d09785..29d90225 100644
--- a/src/hooks/useRiveList.ts
+++ b/src/hooks/useRiveList.ts
@@ -1,3 +1,5 @@
+// TODO: migrate length/getInstanceAt to async equivalents
+/* eslint-disable @typescript-eslint/no-deprecated */
import { useCallback, useEffect, useState, useMemo } from 'react';
import type { ViewModelInstance } from '../specs/ViewModel.nitro';
import type { UseRiveListResult } from '../types';
diff --git a/src/hooks/useViewModelInstance.ts b/src/hooks/useViewModelInstance.ts
index 2fcaeff3..8534e473 100644
--- a/src/hooks/useViewModelInstance.ts
+++ b/src/hooks/useViewModelInstance.ts
@@ -1,3 +1,5 @@
+// TODO: migrate createInstance/createInstanceByName/etc to async equivalents
+/* eslint-disable @typescript-eslint/no-deprecated */
import { useMemo, useEffect, useRef } from 'react';
import type { ViewModel, ViewModelInstance } from '../specs/ViewModel.nitro';
import type { RiveFile } from '../specs/RiveFile.nitro';