1- import { useState , useEffect } from 'react' ;
1+ import { useMemo , useEffect , useRef } from 'react' ;
22import type { ViewModel , ViewModelInstance } from '../specs/ViewModel.nitro' ;
33import type { RiveFile } from '../specs/RiveFile.nitro' ;
44import 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 */
69114export 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