// TODO: migrate createInstance/createInstanceByName/etc to async equivalents
/* eslint-disable @typescript-eslint/no-deprecated */
import { useMemo, useRef } from 'react';
import type { ViewModel, ViewModelInstance } from '../specs/ViewModel.nitro';
import type { RiveFile } from '../specs/RiveFile.nitro';
import type { RiveViewRef } from '../index';
import { callDispose } from '../core/callDispose';
import { ArtboardByName } from '../specs/ArtboardBy';
import { useDisposableMemo } from './useDisposableMemo';
import { useViewModelInstanceAsync } from './useViewModelInstanceAsync';
interface UseViewModelInstanceBaseParams {
/**
* Create the instance via the async runtime APIs (off the JS thread).
* While creation is in flight the hook reports `isLoading: true` with an
* `undefined` instance. Will become the default in the next major.
*
* Must stay constant for the lifetime of the component — it selects
* between two hook implementations.
*/
async?: boolean;
/**
* If true, throws an error when the instance cannot be obtained.
* This is useful with Error Boundaries and ensures TypeScript knows
* the return value is non-null.
*/
required?: boolean;
/**
* Called when a new instance is created, before the hook exposes it —
* synchronously during render without `async: true`, or right after the
* instance resolves (before it is published) with `async: true`.
* Note: This callback is excluded from deps - changing it won't recreate the instance.
*/
onInit?: (instance: ViewModelInstance) => void;
}
interface UseViewModelInstanceFileBaseParams extends UseViewModelInstanceBaseParams {
/**
* The ViewModel instance name (uses `createInstanceByName()`).
* If not provided, creates the default instance.
*/
instanceName?: string;
}
/**
* Use the ViewModel assigned to the default artboard.
*/
interface UseViewModelInstanceFileDefault extends UseViewModelInstanceFileBaseParams {
artboardName?: never;
viewModelName?: never;
}
/**
* Use the ViewModel assigned to a specific artboard.
*/
interface UseViewModelInstanceFileByArtboard extends UseViewModelInstanceFileBaseParams {
/**
* Get the ViewModel assigned to this artboard.
*/
artboardName: string;
viewModelName?: never;
}
/**
* Use a ViewModel by name (file-wide lookup).
* ViewModels are defined at the file level, not per-artboard.
*/
interface UseViewModelInstanceFileByViewModelName extends UseViewModelInstanceFileBaseParams {
artboardName?: never;
/**
* The name of the ViewModel to use (uses `viewModelByName()`).
* ViewModels are defined at the file level and looked up by name across the entire file.
*/
viewModelName: string;
}
export type UseViewModelInstanceFileParams =
| UseViewModelInstanceFileDefault
| UseViewModelInstanceFileByArtboard
| UseViewModelInstanceFileByViewModelName;
export interface UseViewModelInstanceViewModelParams extends UseViewModelInstanceBaseParams {
/**
* The ViewModel instance name (uses `createInstanceByName()`).
* If not provided, creates the default instance.
*/
name?: string;
/**
* Create a new (blank) instance from the ViewModel.
*/
useNew?: boolean;
}
export type UseViewModelInstanceRefParams = UseViewModelInstanceBaseParams;
type ViewModelSource = ViewModel | RiveFile | RiveViewRef;
function isRiveViewRef(
source: ViewModelSource | null | undefined
): source is RiveViewRef {
return source != null && 'getViewModelInstance' in source;
}
function isRiveFile(
source: ViewModelSource | null | undefined
): source is RiveFile {
return source != null && 'defaultArtboardViewModel' in source;
}
type CreateInstanceResult = {
instance: ViewModelInstance | null | undefined;
needsDispose: boolean;
error?: string;
};
function createInstance(
source: ViewModelSource | null | undefined,
instanceName: string | undefined,
artboardName: string | undefined,
viewModelName: string | undefined,
useNew: boolean
): CreateInstanceResult {
if (!source) {
return { instance: undefined, needsDispose: false };
}
if (isRiveViewRef(source)) {
const vmi = source.getViewModelInstance();
return { instance: vmi ?? null, needsDispose: false };
}
if (isRiveFile(source)) {
let viewModel: ViewModel | undefined;
if (viewModelName) {
viewModel = source.viewModelByName(viewModelName);
if (!viewModel) {
return {
instance: null,
needsDispose: false,
error: `ViewModel '${viewModelName}' not found`,
};
}
} else {
viewModel = source.defaultArtboardViewModel(
artboardName ? ArtboardByName(artboardName) : undefined
);
if (!viewModel) {
if (artboardName) {
return {
instance: null,
needsDispose: false,
error: `Artboard '${artboardName}' not found or has no ViewModel`,
};
}
return { instance: null, needsDispose: false };
}
}
let vmi: ViewModelInstance | undefined;
if (instanceName) {
try {
vmi = viewModel.createInstanceByName(instanceName);
} catch (e) {
console.warn(`createInstanceByName('${instanceName}') failed:`, e);
}
} else {
vmi = viewModel.createDefaultInstance();
}
if (!vmi && instanceName) {
return {
instance: null,
needsDispose: false,
error: `ViewModel instance '${instanceName}' not found`,
};
}
return { instance: vmi ?? null, needsDispose: true };
}
// ViewModel source
let vmi: ViewModelInstance | undefined;
if (instanceName) {
try {
vmi = source.createInstanceByName(instanceName);
} catch (e) {
console.warn(`createInstanceByName('${instanceName}') failed:`, e);
}
if (!vmi) {
return {
instance: null,
needsDispose: false,
error: `ViewModel instance '${instanceName}' not found`,
};
}
} else if (useNew) {
vmi = source.createInstance();
} else {
vmi = source.createDefaultInstance();
}
return { instance: vmi ?? null, needsDispose: true };
}
export type UseViewModelInstanceResult =
| { instance: ViewModelInstance; isLoading: false; error: null }
| { instance: null; isLoading: false; error: Error }
| { instance: null; isLoading: false; error: null }
| { instance: undefined; isLoading: true; error: null };
/**
* Result of {@link useViewModelInstance} when `required: true` is set.
* The `null` (error/absent) case is removed — instead the hook throws once the
* instance resolves to `null`, leaving only the ready and loading states.
*/
export type UseViewModelInstanceRequiredResult =
| { instance: ViewModelInstance; isLoading: false; error: null }
| { instance: undefined; isLoading: true; error: null };
/**
* Hook for getting a ViewModelInstance from a RiveFile, ViewModel, or RiveViewRef.
*
* Pass `async: true` to create the instance via the async runtime APIs,
* resolving off the JS thread. The instance is then not available on the
* first render — guard on the result:
*
* ```tsx
* const { riveFile, error: fileError } = useRiveFile(require('./animation.riv'));
* const { instance, isLoading, error } = useViewModelInstance(riveFile, { async: true });
* if (fileError || error) return ;
* if (isLoading || !instance) return ;
* // ...
*
* ```
*
* Without `async: true` (deprecated) the instance is created synchronously
* during render via deprecated runtime APIs that block the JS thread. The
* async path will become the default in the next major.
*
* In both modes, a `null` source settles to a terminal
* `{ instance: null, isLoading: false }` while an `undefined` source keeps
* the hook loading. This mirrors {@link useRiveFile} (`riveFile: undefined`
* while loading, `null` on error) and `useRive` (`riveViewRef: undefined`
* until the view is ready, `null` on failure) — so when chaining, check the
* upstream hook's own `error`, since this hook cannot observe why the source
* is absent.
*
* @param source - The RiveFile, ViewModel, or RiveViewRef to get an instance from
* @param params - Configuration for which instance to retrieve
* @returns An object with `instance`, `isLoading`, and `error` (discriminated union)
*
* @example
* ```tsx
* // From RiveFile (get default instance)
* const { riveFile } = useRiveFile(require('./animation.riv'));
* const { instance, isLoading } = useViewModelInstance(riveFile, { async: true });
* ```
*
* @example
* ```tsx
* // From RiveFile with specific instance name
* const { instance } = useViewModelInstance(riveFile, { async: true, instanceName: 'PersonInstance' });
* ```
*
* @example
* ```tsx
* // From RiveFile with specific ViewModel name
* const { instance } = useViewModelInstance(riveFile, { async: true, viewModelName: 'Settings' });
* ```
*
* @example
* ```tsx
* // From RiveFile with specific artboard
* const { instance } = useViewModelInstance(riveFile, { async: true, artboardName: 'MainArtboard' });
* ```
*
* @example
* ```tsx
* // From RiveViewRef (waits for the view's auto-bound instance)
* const { riveViewRef, setHybridRef } = useRive();
* const { instance } = useViewModelInstance(riveViewRef, { async: true });
* ```
*
* @example
* ```tsx
* // Create a new blank instance from ViewModel
* const { instance } = useViewModelInstance(viewModel, { async: true, useNew: true });
* ```
*
* @example
* ```tsx
* // With required: true (throws once resolved to null, use with Error Boundary).
* // Note: instance is still `undefined` while loading — guard on isLoading.
* // The required-narrowed return type applies only when the source's TYPE is
* // non-nullable; with a nullable source (e.g. straight from useRiveFile) the
* // call resolves to the standard overload — the runtime throw still applies.
* const { instance, isLoading } = useViewModelInstance(riveFile, { async: true, required: true });
* ```
*
* @example
* ```tsx
* // With onInit to set initial values before the instance is exposed or bound
* const { instance } = useViewModelInstance(riveFile, {
* async: true,
* onInit: (vmi) => {
* vmi.numberProperty('count')?.set(initialCount);
* }
* });
* ```
*/
// RiveFile overloads
export function useViewModelInstance(
source: RiveFile,
params: UseViewModelInstanceFileParams & { async: true; required: true }
): UseViewModelInstanceRequiredResult;
export function useViewModelInstance(
source: RiveFile | null | undefined,
params: UseViewModelInstanceFileParams & { async: true }
): UseViewModelInstanceResult;
/** @deprecated Pass `async: true` — without it the instance is created synchronously via deprecated runtime APIs that block the JS thread. `async: true` becomes the default in the next major. If your params object's `async` widened to `boolean`, re-pin it at the call site: `{ ...params, async: true }`. */
export function useViewModelInstance(
source: RiveFile,
params: UseViewModelInstanceFileParams & { required: true }
): UseViewModelInstanceRequiredResult;
/** @deprecated Pass `async: true` — without it the instance is created synchronously via deprecated runtime APIs that block the JS thread. `async: true` becomes the default in the next major. If your params object's `async` widened to `boolean`, re-pin it at the call site: `{ ...params, async: true }`. */
export function useViewModelInstance(
source: RiveFile | null | undefined,
params?: UseViewModelInstanceFileParams
): UseViewModelInstanceResult;
// ViewModel overloads
export function useViewModelInstance(
source: ViewModel,
params: UseViewModelInstanceViewModelParams & { async: true; required: true }
): UseViewModelInstanceRequiredResult;
export function useViewModelInstance(
source: ViewModel | null | undefined,
params: UseViewModelInstanceViewModelParams & { async: true }
): UseViewModelInstanceResult;
/** @deprecated Pass `async: true` — without it the instance is created synchronously via deprecated runtime APIs that block the JS thread. `async: true` becomes the default in the next major. If your params object's `async` widened to `boolean`, re-pin it at the call site: `{ ...params, async: true }`. */
export function useViewModelInstance(
source: ViewModel,
params: UseViewModelInstanceViewModelParams & { required: true }
): UseViewModelInstanceRequiredResult;
/** @deprecated Pass `async: true` — without it the instance is created synchronously via deprecated runtime APIs that block the JS thread. `async: true` becomes the default in the next major. If your params object's `async` widened to `boolean`, re-pin it at the call site: `{ ...params, async: true }`. */
export function useViewModelInstance(
source: ViewModel | null | undefined,
params?: UseViewModelInstanceViewModelParams
): UseViewModelInstanceResult;
// RiveViewRef overloads
export function useViewModelInstance(
source: RiveViewRef,
params: UseViewModelInstanceRefParams & { async: true; required: true }
): UseViewModelInstanceRequiredResult;
export function useViewModelInstance(
source: RiveViewRef | null | undefined,
params: UseViewModelInstanceRefParams & { async: true }
): UseViewModelInstanceResult;
/** @deprecated Pass `async: true` — without it the instance is created synchronously via deprecated runtime APIs that block the JS thread. `async: true` becomes the default in the next major. If your params object's `async` widened to `boolean`, re-pin it at the call site: `{ ...params, async: true }`. */
export function useViewModelInstance(
source: RiveViewRef,
params: UseViewModelInstanceRefParams & { required: true }
): UseViewModelInstanceRequiredResult;
/** @deprecated Pass `async: true` — without it the instance is created synchronously via deprecated runtime APIs that block the JS thread. `async: true` becomes the default in the next major. If your params object's `async` widened to `boolean`, re-pin it at the call site: `{ ...params, async: true }`. */
export function useViewModelInstance(
source: RiveViewRef | null | undefined,
params?: UseViewModelInstanceRefParams
): UseViewModelInstanceResult;
// Implementation
export function useViewModelInstance(
source: ViewModelSource | null | undefined,
params?:
| UseViewModelInstanceFileParams
| UseViewModelInstanceViewModelParams
| UseViewModelInstanceRefParams
): UseViewModelInstanceResult {
const isAsync = params?.async ?? false;
// The flag selects between two hook implementations (different hook
// orders), so it must stay constant for the lifetime of the component —
// documented on the param. React's own failure for a flip is the cryptic
// "Rendered more hooks than during the previous render"; explain first.
const initialAsyncRef = useRef(isAsync);
if (initialAsyncRef.current !== isAsync) {
console.error(
'useViewModelInstance: the `async` param changed between renders ' +
`(${String(initialAsyncRef.current)} → ${String(isAsync)}). It selects ` +
'between two hook implementations, so it must stay constant for the ' +
'lifetime of the component; remount (e.g. change `key`) to switch modes.'
);
initialAsyncRef.current = isAsync;
}
if (isAsync) {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useViewModelInstanceAsync(
source as RiveFile | null | undefined,
params as UseViewModelInstanceFileParams
);
}
// eslint-disable-next-line react-hooks/rules-of-hooks
return useViewModelInstanceSync(source, params);
}
function useViewModelInstanceSync(
source: ViewModelSource | null | undefined,
params?:
| UseViewModelInstanceFileParams
| UseViewModelInstanceViewModelParams
| UseViewModelInstanceRefParams
): UseViewModelInstanceResult {
const fileInstanceName = (params as { instanceName?: string } | undefined)
?.instanceName;
const viewModelInstanceName = (params as { name?: string } | undefined)?.name;
const instanceName = fileInstanceName ?? viewModelInstanceName;
const artboardName = (params as UseViewModelInstanceFileParams | undefined)
?.artboardName;
const viewModelName = (params as UseViewModelInstanceFileParams | undefined)
?.viewModelName;
const useNew =
(params as UseViewModelInstanceViewModelParams | undefined)?.useNew ??
false;
const required = params?.required ?? false;
const onInit = params?.onInit;
const onInitRef = useRef(onInit);
onInitRef.current = onInit;
const result = useDisposableMemo(
() => {
const created = createInstance(
source,
instanceName,
artboardName,
viewModelName,
useNew
);
if (created.instance && onInitRef.current) {
onInitRef.current(created.instance);
}
return created;
},
(r) => {
if (r.needsDispose && r.instance) {
callDispose(r.instance);
}
},
[source, instanceName, artboardName, viewModelName, useNew]
);
const error = useMemo(
() => (result.error ? new Error(result.error) : null),
[result.error]
);
if (result.instance === undefined && source === null) {
// Source resolved to absent/failed rather than pending (`useRiveFile`
// returns null on load error, undefined while loading) — settle to a
// terminal null instead of reporting isLoading forever, mirroring the
// async path.
if (required) {
throw new Error(
'useViewModelInstance: source is null — the file or view failed to ' +
"resolve upstream (if it comes from useRiveFile or useRive, check that hook's error)."
);
}
return { instance: null, isLoading: false, error: null };
}
if (required && result.instance === null) {
throw new Error(
result.error
? `useViewModelInstance: ${result.error}`
: 'useViewModelInstance: Failed to get ViewModelInstance. ' +
'Ensure the source has a valid ViewModel and instance available.'
);
}
if (result.instance) {
return { instance: result.instance, isLoading: false, error: null };
}
if (result.instance === undefined) {
// Source not resolved yet (e.g. the file is still loading).
return { instance: undefined, isLoading: true, error: null };
}
return { instance: null, isLoading: false, error };
}