Skip to content

Commit e129473

Browse files
committed
feat: add useViewModelInstance hook with RiveFile support
1 parent 2a88413 commit e129473

3 files changed

Lines changed: 123 additions & 10 deletions

File tree

example/src/pages/MenuListExample.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import {
77
TextInput,
88
ScrollView,
99
} from 'react-native';
10-
import { useMemo, useRef } from 'react';
10+
import { useRef, useMemo } from 'react';
1111
import {
1212
Fit,
1313
RiveView,
1414
type ViewModelInstance,
1515
type RiveFile,
1616
useRiveFile,
1717
useRiveList,
18+
useViewModelInstance,
1819
} from '@rive-app/react-native';
1920
import { type Metadata } from '../helpers/metadata';
2021

@@ -37,18 +38,12 @@ export default function MenuListExample() {
3738
}
3839

3940
function WithViewModelSetup({ file }: { file: RiveFile }) {
40-
const viewModel = useMemo(() => file.viewModelByName('main'), [file]);
41-
const instance = useMemo(
42-
() => viewModel?.createDefaultInstance(),
43-
[viewModel]
44-
);
41+
const instance = useViewModelInstance(file, { viewModelName: 'main' });
4542

46-
if (!instance || !viewModel) {
43+
if (!instance) {
4744
return (
4845
<Text style={styles.errorText}>
49-
{!viewModel
50-
? "No view model 'main' found"
51-
: 'Failed to create view model instance'}
46+
Failed to create view model instance for 'main'
5247
</Text>
5348
);
5449
}

src/hooks/useViewModelInstance.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { useState, useEffect, useMemo } from 'react';
2+
import type { ViewModel, ViewModelInstance } from '../specs/ViewModel.nitro';
3+
import type { RiveFile } from '../specs/RiveFile.nitro';
4+
5+
export interface UseViewModelInstanceParams {
6+
/**
7+
* The name of the ViewModel to get from the RiveFile.
8+
* Only used when passing a RiveFile as the first argument.
9+
*/
10+
viewModelName?: string;
11+
/**
12+
* Get a specifically named instance from the ViewModel.
13+
*/
14+
name?: string;
15+
/**
16+
* Get the default instance from the ViewModel.
17+
* @default true
18+
*/
19+
useDefault?: boolean;
20+
/**
21+
* Create a new (blank) instance from the ViewModel.
22+
*/
23+
useNew?: boolean;
24+
}
25+
26+
type ViewModelSource = ViewModel | RiveFile | null | undefined;
27+
28+
function isRiveFile(source: ViewModelSource): source is RiveFile {
29+
return source !== null && source !== undefined && 'viewModelByName' in source;
30+
}
31+
32+
/**
33+
* Hook for getting a ViewModelInstance from a ViewModel or RiveFile.
34+
*
35+
* Resolution order:
36+
* 1. If `name` is provided, gets instance by name
37+
* 2. If `useNew` is true, creates a new instance
38+
* 3. Otherwise, gets the default instance
39+
*
40+
* @param source - The ViewModel or RiveFile to get an instance from
41+
* @param params - Configuration for which instance to retrieve
42+
* @returns The ViewModelInstance or null if not found
43+
*
44+
* @example
45+
* ```tsx
46+
* // From RiveFile (recommended)
47+
* const instance = useViewModelInstance(riveFile, { viewModelName: 'main' });
48+
* ```
49+
*
50+
* @example
51+
* ```tsx
52+
* // From ViewModel
53+
* const viewModel = file.viewModelByName('main');
54+
* const instance = useViewModelInstance(viewModel);
55+
* ```
56+
*
57+
* @example
58+
* ```tsx
59+
* // Create a new blank instance
60+
* const instance = useViewModelInstance(riveFile, {
61+
* viewModelName: 'TodoItem',
62+
* useNew: true
63+
* });
64+
* ```
65+
*/
66+
export function useViewModelInstance(
67+
source: ViewModelSource,
68+
params?: UseViewModelInstanceParams
69+
): ViewModelInstance | null {
70+
const [instance, setInstance] = useState<ViewModelInstance | null>(null);
71+
72+
const viewModelName = params?.viewModelName;
73+
const name = params?.name;
74+
const useDefault = params?.useDefault ?? true;
75+
const useNew = params?.useNew ?? false;
76+
77+
const viewModel = useMemo(() => {
78+
if (!source) return null;
79+
if (isRiveFile(source)) {
80+
if (!viewModelName) {
81+
console.warn(
82+
'useViewModelInstance: viewModelName is required when passing a RiveFile'
83+
);
84+
return null;
85+
}
86+
return source.viewModelByName(viewModelName) ?? null;
87+
}
88+
return source;
89+
}, [source, viewModelName]);
90+
91+
useEffect(() => {
92+
if (!viewModel) {
93+
setInstance(null);
94+
return;
95+
}
96+
97+
let vmi: ViewModelInstance | undefined;
98+
99+
if (name) {
100+
vmi = viewModel.createInstanceByName(name);
101+
} else if (useNew) {
102+
vmi = viewModel.createInstance();
103+
} else if (useDefault) {
104+
vmi = viewModel.createDefaultInstance();
105+
}
106+
107+
setInstance(vmi ?? null);
108+
109+
return () => {
110+
if (vmi) {
111+
vmi.dispose();
112+
}
113+
};
114+
}, [viewModel, name, useDefault, useNew]);
115+
116+
return instance;
117+
}

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export { useRiveEnum } from './hooks/useRiveEnum';
4949
export { useRiveColor } from './hooks/useRiveColor';
5050
export { useRiveTrigger } from './hooks/useRiveTrigger';
5151
export { useRiveList } from './hooks/useRiveList';
52+
export { useViewModelInstance } from './hooks/useViewModelInstance';
5253
export { useRiveFile } from './hooks/useRiveFile';
5354
export { type RiveFileInput } from './hooks/useRiveFile';
5455
export { DataBindMode };

0 commit comments

Comments
 (0)