Skip to content

Commit 6c47e11

Browse files
committed
fix!: useViewModelInstance returns { instance, error } instead of instance | null
Consumers can now distinguish between no-source (instance: null, error: null), lookup error (instance: null, error: string), and success (instance: VMI, error: null).
1 parent b65359e commit 6c47e11

6 files changed

Lines changed: 54 additions & 34 deletions

File tree

example/__tests__/hooks.harness.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function UseViewModelInstanceTestComponent({
7070
file: RiveFile;
7171
context: UseViewModelInstanceContext;
7272
}) {
73-
const instance = useViewModelInstance(file);
73+
const { instance } = useViewModelInstance(file);
7474

7575
const age = useMemo(() => {
7676
if (!instance) return undefined;

example/src/demos/QuickStart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default function QuickStart() {
2323
require('../../assets/rive/quick_start.riv')
2424
);
2525
const { riveViewRef, setHybridRef } = useRive();
26-
const viewModelInstance = useViewModelInstance(riveFile, {
26+
const { instance: viewModelInstance } = useViewModelInstance(riveFile, {
2727
onInit: (vmi) => vmi.numberProperty('health')!.set(9),
2828
});
2929

example/src/exercisers/MenuListExample.tsx

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

4141
function MenuList({ file }: { file: RiveFile }) {
42-
const instance = useViewModelInstance(file, { required: true });
42+
const { instance } = useViewModelInstance(file, { required: true });
4343

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

src/hooks/__tests__/useViewModelInstance.test.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ describe('useViewModelInstance - RiveFile with instanceName parameter', () => {
8686
'PersonInstance'
8787
);
8888
expect(defaultViewModel.createDefaultInstance).not.toHaveBeenCalled();
89-
expect(result.current).toBe(personInstance);
89+
expect(result.current.instance).toBe(personInstance);
90+
expect(result.current.error).toBeNull();
9091
});
9192

9293
it('should use defaultArtboardViewModel and createDefaultInstance when no instanceName provided', () => {
@@ -102,7 +103,8 @@ describe('useViewModelInstance - RiveFile with instanceName parameter', () => {
102103
);
103104
expect(defaultViewModel.createDefaultInstance).toHaveBeenCalled();
104105
expect(defaultViewModel.createInstanceByName).not.toHaveBeenCalled();
105-
expect(result.current).toBe(defaultInstance);
106+
expect(result.current.instance).toBe(defaultInstance);
107+
expect(result.current.error).toBeNull();
106108
});
107109

108110
it('should return null when instance name not found and required is false', () => {
@@ -116,7 +118,8 @@ describe('useViewModelInstance - RiveFile with instanceName parameter', () => {
116118
useViewModelInstance(mockRiveFile, { instanceName: 'NonExistent' })
117119
);
118120

119-
expect(result.current).toBeNull();
121+
expect(result.current.instance).toBeNull();
122+
expect(result.current.error).toContain('not found');
120123
});
121124

122125
it('should throw when instance name not found and required is true', () => {
@@ -145,7 +148,8 @@ describe('useViewModelInstance - RiveFile with instanceName parameter', () => {
145148
useViewModelInstance(mockRiveFile, { artboardName: 'MissingArtboard' })
146149
);
147150

148-
expect(result.current).toBeNull();
151+
expect(result.current.instance).toBeNull();
152+
expect(result.current.error).toContain('not found');
149153
});
150154

151155
it('should throw when artboardName not found and required is true', () => {
@@ -203,7 +207,8 @@ describe('useViewModelInstance - RiveFile with artboardName parameter', () => {
203207
name: 'MainArtboard',
204208
});
205209
expect(mainArtboardViewModel.createDefaultInstance).toHaveBeenCalled();
206-
expect(result.current).toBe(mainInstance);
210+
expect(result.current.instance).toBe(mainInstance);
211+
expect(result.current.error).toBeNull();
207212
});
208213

209214
it('should combine artboardName and instanceName to get specific instance from specific artboard', () => {
@@ -230,7 +235,8 @@ describe('useViewModelInstance - RiveFile with artboardName parameter', () => {
230235
expect(mainArtboardViewModel.createInstanceByName).toHaveBeenCalledWith(
231236
'SpecificInstance'
232237
);
233-
expect(result.current).toBe(specificInstance);
238+
expect(result.current.instance).toBe(specificInstance);
239+
expect(result.current.error).toBeNull();
234240
});
235241
});
236242

@@ -252,7 +258,8 @@ describe('useViewModelInstance - RiveFile with viewModelName parameter', () => {
252258
expect(mockRiveFile.viewModelByName).toHaveBeenCalledWith('Settings');
253259
expect(mockRiveFile.defaultArtboardViewModel).not.toHaveBeenCalled();
254260
expect(settingsViewModel.createDefaultInstance).toHaveBeenCalled();
255-
expect(result.current).toBe(settingsInstance);
261+
expect(result.current.instance).toBe(settingsInstance);
262+
expect(result.current.error).toBeNull();
256263
});
257264

258265
it('should return null when viewModelName not found and required is false', () => {
@@ -264,7 +271,8 @@ describe('useViewModelInstance - RiveFile with viewModelName parameter', () => {
264271
useViewModelInstance(mockRiveFile, { viewModelName: 'NonExistent' })
265272
);
266273

267-
expect(result.current).toBeNull();
274+
expect(result.current.instance).toBeNull();
275+
expect(result.current.error).toContain('not found');
268276
});
269277

270278
it('should throw when viewModelName not found and required is true', () => {
@@ -303,7 +311,8 @@ describe('useViewModelInstance - RiveFile with viewModelName parameter', () => {
303311
expect(settingsViewModel.createInstanceByName).toHaveBeenCalledWith(
304312
'UserSettings'
305313
);
306-
expect(result.current).toBe(specificInstance);
314+
expect(result.current.instance).toBe(specificInstance);
315+
expect(result.current.error).toBeNull();
307316
});
308317
});
309318

@@ -320,7 +329,8 @@ describe('useViewModelInstance - ViewModel source', () => {
320329

321330
expect(mockViewModel.createInstanceByName).toHaveBeenCalledWith('Gordon');
322331
expect(mockViewModel.createDefaultInstance).not.toHaveBeenCalled();
323-
expect(result.current).toBe(namedInstance);
332+
expect(result.current.instance).toBe(namedInstance);
333+
expect(result.current.error).toBeNull();
324334
});
325335

326336
it('should use createInstance when useNew is true', () => {
@@ -334,7 +344,8 @@ describe('useViewModelInstance - ViewModel source', () => {
334344

335345
expect(mockViewModel.createInstance).toHaveBeenCalled();
336346
expect(mockViewModel.createDefaultInstance).not.toHaveBeenCalled();
337-
expect(result.current).toBe(newInstance);
347+
expect(result.current.instance).toBe(newInstance);
348+
expect(result.current.error).toBeNull();
338349
});
339350

340351
it('should use createDefaultInstance when no params provided', () => {
@@ -344,6 +355,7 @@ describe('useViewModelInstance - ViewModel source', () => {
344355
const { result } = renderHook(() => useViewModelInstance(mockViewModel));
345356

346357
expect(mockViewModel.createDefaultInstance).toHaveBeenCalled();
347-
expect(result.current).toBe(defaultInstance);
358+
expect(result.current.instance).toBe(defaultInstance);
359+
expect(result.current.error).toBeNull();
348360
});
349361
});

src/hooks/useViewModelInstance.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -176,73 +176,78 @@ function createInstance(
176176
return { instance: vmi ?? null, needsDispose: true };
177177
}
178178

179+
export type UseViewModelInstanceResult = {
180+
instance: ViewModelInstance | null;
181+
error: string | null;
182+
};
183+
179184
/**
180185
* Hook for getting a ViewModelInstance from a RiveFile, ViewModel, or RiveViewRef.
181186
*
182187
* @param source - The RiveFile, ViewModel, or RiveViewRef to get an instance from
183188
* @param params - Configuration for which instance to retrieve
184-
* @returns The ViewModelInstance or null if not found
189+
* @returns An object with `instance` (the ViewModelInstance or null) and `error` (string or null)
185190
*
186191
* @example
187192
* ```tsx
188193
* // From RiveFile (get default instance)
189194
* const { riveFile } = useRiveFile(require('./animation.riv'));
190-
* const instance = useViewModelInstance(riveFile);
195+
* const { instance } = useViewModelInstance(riveFile);
191196
* ```
192197
*
193198
* @example
194199
* ```tsx
195200
* // From RiveFile with specific instance name
196201
* const { riveFile } = useRiveFile(require('./animation.riv'));
197-
* const instance = useViewModelInstance(riveFile, { instanceName: 'PersonInstance' });
202+
* const { instance } = useViewModelInstance(riveFile, { instanceName: 'PersonInstance' });
198203
* ```
199204
*
200205
* @example
201206
* ```tsx
202207
* // From RiveFile with specific ViewModel name
203208
* const { riveFile } = useRiveFile(require('./animation.riv'));
204-
* const instance = useViewModelInstance(riveFile, { viewModelName: 'Settings' });
209+
* const { instance } = useViewModelInstance(riveFile, { viewModelName: 'Settings' });
205210
* ```
206211
*
207212
* @example
208213
* ```tsx
209214
* // From RiveFile with specific artboard
210215
* const { riveFile } = useRiveFile(require('./animation.riv'));
211-
* const instance = useViewModelInstance(riveFile, { artboardName: 'MainArtboard' });
216+
* const { instance } = useViewModelInstance(riveFile, { artboardName: 'MainArtboard' });
212217
* ```
213218
*
214219
* @example
215220
* ```tsx
216221
* // From RiveViewRef (get auto-bound instance)
217222
* const { riveViewRef, setHybridRef } = useRive();
218-
* const instance = useViewModelInstance(riveViewRef);
223+
* const { instance } = useViewModelInstance(riveViewRef);
219224
* ```
220225
*
221226
* @example
222227
* ```tsx
223228
* // From ViewModel
224229
* const viewModel = file.viewModelByName('main');
225-
* const instance = useViewModelInstance(viewModel);
230+
* const { instance } = useViewModelInstance(viewModel);
226231
* ```
227232
*
228233
* @example
229234
* ```tsx
230235
* // Create a new blank instance from ViewModel
231236
* const viewModel = file.viewModelByName('TodoItem');
232-
* const newInstance = useViewModelInstance(viewModel, { useNew: true });
237+
* const { instance: newInstance } = useViewModelInstance(viewModel, { useNew: true });
233238
* ```
234239
*
235240
* @example
236241
* ```tsx
237242
* // With required: true (throws if null, use with Error Boundary)
238-
* const instance = useViewModelInstance(riveFile, { required: true });
243+
* const { instance } = useViewModelInstance(riveFile, { required: true });
239244
* // instance is guaranteed to be non-null here
240245
* ```
241246
*
242247
* @example
243248
* ```tsx
244249
* // With onInit to set initial values synchronously
245-
* const instance = useViewModelInstance(riveFile, {
250+
* const { instance } = useViewModelInstance(riveFile, {
246251
* onInit: (vmi) => {
247252
* vmi.numberProperty('count').set(initialCount);
248253
* vmi.stringProperty('name').set(userName);
@@ -255,31 +260,31 @@ function createInstance(
255260
export function useViewModelInstance(
256261
source: RiveFile,
257262
params: UseViewModelInstanceFileParams & { required: true }
258-
): ViewModelInstance;
263+
): { instance: ViewModelInstance; error: null };
259264
export function useViewModelInstance(
260265
source: RiveFile | null,
261266
params?: UseViewModelInstanceFileParams
262-
): ViewModelInstance | null;
267+
): UseViewModelInstanceResult;
263268

264269
// ViewModel overloads
265270
export function useViewModelInstance(
266271
source: ViewModel,
267272
params: UseViewModelInstanceViewModelParams & { required: true }
268-
): ViewModelInstance;
273+
): { instance: ViewModelInstance; error: null };
269274
export function useViewModelInstance(
270275
source: ViewModel | null,
271276
params?: UseViewModelInstanceViewModelParams
272-
): ViewModelInstance | null;
277+
): UseViewModelInstanceResult;
273278

274279
// RiveViewRef overloads
275280
export function useViewModelInstance(
276281
source: RiveViewRef,
277282
params: UseViewModelInstanceRefParams & { required: true }
278-
): ViewModelInstance;
283+
): { instance: ViewModelInstance; error: null };
279284
export function useViewModelInstance(
280285
source: RiveViewRef | null,
281286
params?: UseViewModelInstanceRefParams
282-
): ViewModelInstance | null;
287+
): UseViewModelInstanceResult;
283288

284289
// Implementation
285290
export function useViewModelInstance(
@@ -288,7 +293,7 @@ export function useViewModelInstance(
288293
| UseViewModelInstanceFileParams
289294
| UseViewModelInstanceViewModelParams
290295
| UseViewModelInstanceRefParams
291-
): ViewModelInstance | null {
296+
): UseViewModelInstanceResult {
292297
const fileInstanceName = (params as { instanceName?: string } | undefined)
293298
?.instanceName;
294299
const viewModelInstanceName = (params as { name?: string } | undefined)?.name;
@@ -356,5 +361,5 @@ export function useViewModelInstance(
356361
);
357362
}
358363

359-
return result.instance;
364+
return { instance: result.instance, error: result.error ?? null };
360365
}

src/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ export { useRiveEnum } from './hooks/useRiveEnum';
5858
export { useRiveColor } from './hooks/useRiveColor';
5959
export { useRiveTrigger } from './hooks/useRiveTrigger';
6060
export { useRiveList } from './hooks/useRiveList';
61-
export { useViewModelInstance } from './hooks/useViewModelInstance';
61+
export {
62+
useViewModelInstance,
63+
type UseViewModelInstanceResult,
64+
} from './hooks/useViewModelInstance';
6265
export { useRiveFile } from './hooks/useRiveFile';
6366
export { type RiveFileInput } from './hooks/useRiveFile';
6467
export { type SetValueAction } from './types';

0 commit comments

Comments
 (0)