Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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',
Expand All @@ -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/',
Expand Down
31 changes: 31 additions & 0 deletions example/__tests__/hooks.harness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<UseRiveNumberTestComponent instance={instance} context={context} />
);

// 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', () => {
Expand Down
22 changes: 5 additions & 17 deletions example/src/demos/DataBindingArtboardsExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string>('Dragon');
const initializedRef = useRef(false);

Expand Down Expand Up @@ -134,14 +126,10 @@ function ArtboardSwapper({
}
};

if (!instance || !viewModel) {
if (!instance) {
return (
<View style={styles.container}>
<Text style={styles.errorText}>
{!viewModel
? 'No view model found in main file'
: 'Failed to create instance'}
</Text>
<ActivityIndicator size="large" color="#0000ff" />
</View>
);
}
Expand Down
2 changes: 1 addition & 1 deletion example/src/demos/QuickStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 6 additions & 14 deletions example/src/exercisers/FontFallbackExample.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useMemo, useEffect } from 'react';
import { useState, useEffect } from 'react';
import {
View,
Text,
Expand All @@ -15,6 +15,7 @@ import {
useRive,
useRiveFile,
useRiveString,
useViewModelInstance,
type FontSource,
type FallbackFont,
} from '@rive-app/react-native';
Expand Down Expand Up @@ -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,
Expand All @@ -280,20 +274,18 @@ function MountedView({ text }: { text: string }) {
riveViewRef?.playIfNeeded();
}, [text, setRiveText, riveViewRef]);

if (isLoading) {
if (isLoading || !instance) {
return (
<View style={styles.riveContainer}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
);
}

if (error || !riveFile || !instance) {
if (error || !riveFile) {
return (
<View style={styles.riveContainer}>
<Text style={styles.errorText}>
{error || 'Failed to set up view model'}
</Text>
<Text style={styles.errorText}>{error || 'Failed to load file'}</Text>
</View>
);
}
Expand Down
33 changes: 22 additions & 11 deletions example/src/exercisers/ManyViewModels.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -84,17 +84,28 @@ export default function ManyViewModels() {
const riveViewRef = useRef<RiveViewRef>(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 () => {
Expand Down
35 changes: 24 additions & 11 deletions example/src/exercisers/MenuListExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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');
Expand All @@ -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);
Expand Down Expand Up @@ -126,7 +139,7 @@ function MenuListContent({
const menuItemLabel = menuItem.stringProperty('label');
if (!menuItemLabel) return;

menuItemLabel.value = label;
menuItemLabel.set(label);
};

return (
Expand Down
26 changes: 8 additions & 18 deletions example/src/exercisers/NestedViewModelExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
<Text style={styles.errorText}>
{!viewModel
? 'No view model found'
: 'Failed to create view model instance'}
</Text>
);
if (!instance) {
return <ActivityIndicator size="large" color="#0000ff" />;
}

return <ReplaceViewModelTest instance={instance} file={file} />;
Expand Down Expand Up @@ -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}`);
Expand Down
19 changes: 5 additions & 14 deletions example/src/exercisers/RiveDataBindingExample.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 (
<Text style={styles.errorText}>
{!viewModel
? 'No view model found'
: 'Failed to create view model instance'}
</Text>
);
if (!instance) {
return <ActivityIndicator size="large" color="#0000ff" />;
}

return <DataBindingExample instance={instance} file={file} />;
Expand Down
Loading
Loading