Skip to content

Commit fb4e14b

Browse files
committed
feat: add required option to useViewModelInstance
Sync computation with useMemo so required throws immediately if instance unavailable.
1 parent 9fb10af commit fb4e14b

2 files changed

Lines changed: 92 additions & 42 deletions

File tree

example/src/pages/MenuListExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default function MenuListExample() {
3838
}
3939

4040
function MenuList({ file }: { file: RiveFile }) {
41-
const instance = useViewModelInstance(file);
41+
const instance = useViewModelInstance(file, { required: true });
4242

4343
if (!instance) {
4444
return <ActivityIndicator size="large" color="#007AFF" />;

src/hooks/useViewModelInstance.ts

Lines changed: 91 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'react';
1+
import { useMemo, useEffect, useRef } from 'react';
22
import type { ViewModel, ViewModelInstance } from '../specs/ViewModel.nitro';
33
import type { RiveFile } from '../specs/RiveFile.nitro';
44
import type { RiveViewRef } from '../index';
@@ -13,24 +13,62 @@ export interface UseViewModelInstanceParams {
1313
* Create a new (blank) instance from the ViewModel.
1414
*/
1515
useNew?: boolean;
16+
/**
17+
* If true, throws an error when the instance cannot be obtained.
18+
* This is useful with Error Boundaries and ensures TypeScript knows
19+
* the return value is non-null.
20+
*/
21+
required?: boolean;
1622
}
1723

18-
type ViewModelSource = ViewModel | RiveFile | RiveViewRef | null | undefined;
24+
type ViewModelSource = ViewModel | RiveFile | RiveViewRef;
1925

20-
function isRiveViewRef(source: ViewModelSource): source is RiveViewRef {
26+
function isRiveViewRef(source: ViewModelSource | null): source is RiveViewRef {
2127
return (
2228
source !== null && source !== undefined && 'getViewModelInstance' in source
2329
);
2430
}
2531

26-
function isRiveFile(source: ViewModelSource): source is RiveFile {
32+
function isRiveFile(source: ViewModelSource | null): source is RiveFile {
2733
return (
2834
source !== null &&
2935
source !== undefined &&
3036
'defaultArtboardViewModel' in source
3137
);
3238
}
3339

40+
function createInstance(
41+
source: ViewModelSource | null,
42+
name: string | undefined,
43+
useNew: boolean
44+
): { instance: ViewModelInstance | null; needsDispose: boolean } {
45+
if (!source) {
46+
return { instance: null, needsDispose: false };
47+
}
48+
49+
if (isRiveViewRef(source)) {
50+
const vmi = source.getViewModelInstance();
51+
return { instance: vmi ?? null, needsDispose: false };
52+
}
53+
54+
if (isRiveFile(source)) {
55+
const viewModel = source.defaultArtboardViewModel();
56+
const vmi = viewModel?.createDefaultInstance();
57+
return { instance: vmi ?? null, needsDispose: true };
58+
}
59+
60+
// ViewModel source
61+
let vmi: ViewModelInstance | undefined;
62+
if (name) {
63+
vmi = source.createInstanceByName(name);
64+
} else if (useNew) {
65+
vmi = source.createInstance();
66+
} else {
67+
vmi = source.createDefaultInstance();
68+
}
69+
return { instance: vmi ?? null, needsDispose: true };
70+
}
71+
3472
/**
3573
* Hook for getting a ViewModelInstance from a RiveFile, ViewModel, or RiveViewRef.
3674
*
@@ -65,56 +103,68 @@ function isRiveFile(source: ViewModelSource): source is RiveFile {
65103
* const viewModel = file.viewModelByName('TodoItem');
66104
* const newInstance = useViewModelInstance(viewModel, { useNew: true });
67105
* ```
106+
*
107+
* @example
108+
* ```tsx
109+
* // With required: true (throws if null, use with Error Boundary)
110+
* const instance = useViewModelInstance(riveFile, { required: true });
111+
* // instance is guaranteed to be non-null here
112+
* ```
68113
*/
69114
export function useViewModelInstance(
70115
source: ViewModelSource,
116+
params: UseViewModelInstanceParams & { required: true }
117+
): ViewModelInstance;
118+
export function useViewModelInstance(
119+
source: ViewModelSource | null,
120+
params?: UseViewModelInstanceParams
121+
): ViewModelInstance | null;
122+
export function useViewModelInstance(
123+
source: ViewModelSource | null,
71124
params?: UseViewModelInstanceParams
72125
): ViewModelInstance | null {
73-
const [instance, setInstance] = useState<ViewModelInstance | null>(null);
74-
75126
const name = params?.name;
76127
const useNew = params?.useNew ?? false;
128+
const required = params?.required ?? false;
77129

78-
useEffect(() => {
79-
if (!source) {
80-
setInstance(null);
81-
return;
82-
}
83-
84-
if (isRiveViewRef(source)) {
85-
const vmi = source.getViewModelInstance();
86-
setInstance(vmi ?? null);
87-
return;
88-
}
130+
const prevInstanceRef = useRef<{
131+
instance: ViewModelInstance | null;
132+
needsDispose: boolean;
133+
} | null>(null);
89134

90-
if (isRiveFile(source)) {
91-
const viewModel = source.defaultArtboardViewModel();
92-
const vmi = viewModel?.createDefaultInstance();
93-
setInstance(vmi ?? null);
94-
return () => {
95-
if (vmi) {
96-
callDispose(vmi);
97-
}
98-
};
99-
}
135+
const result = useMemo(() => {
136+
return createInstance(source, name, useNew);
137+
}, [source, name, useNew]);
100138

101-
// ViewModel source
102-
let vmi: ViewModelInstance | undefined;
103-
if (name) {
104-
vmi = source.createInstanceByName(name);
105-
} else if (useNew) {
106-
vmi = source.createInstance();
107-
} else {
108-
vmi = source.createDefaultInstance();
109-
}
110-
setInstance(vmi ?? null);
139+
// Dispose previous instance if it changed and needed disposal
140+
if (
141+
prevInstanceRef.current &&
142+
prevInstanceRef.current.instance !== result.instance &&
143+
prevInstanceRef.current.needsDispose &&
144+
prevInstanceRef.current.instance
145+
) {
146+
callDispose(prevInstanceRef.current.instance);
147+
}
148+
prevInstanceRef.current = result;
111149

150+
// Cleanup on unmount
151+
useEffect(() => {
112152
return () => {
113-
if (vmi) {
114-
callDispose(vmi);
153+
if (
154+
prevInstanceRef.current?.needsDispose &&
155+
prevInstanceRef.current.instance
156+
) {
157+
callDispose(prevInstanceRef.current.instance);
115158
}
116159
};
117-
}, [source, name, useNew]);
160+
}, []);
161+
162+
if (required && result.instance === null) {
163+
throw new Error(
164+
'useViewModelInstance: Failed to get ViewModelInstance. ' +
165+
'Ensure the source has a valid ViewModel and instance available.'
166+
);
167+
}
118168

119-
return instance;
169+
return result.instance;
120170
}

0 commit comments

Comments
 (0)