fix: hot reload crash

This commit is contained in:
Adam
2025-04-24 15:33:07 -07:00
parent bef03fc403
commit 3866cb9e06
14 changed files with 633 additions and 900 deletions

View File

@@ -1,83 +1,95 @@
import { useState, useEffect, useRef } from 'react';
import { EventType, Rive, ViewModel } from '@rive-app/canvas';
import { Rive, ViewModel, EventType } from '@rive-app/canvas';
import { UseViewModelParameters } from '../types';
const defaultParams: UseViewModelParameters = { useDefault: true };
const equal = (
params: UseViewModelParameters | null,
to: UseViewModelParameters | null
): boolean => {
if (!params || !to) {
function areParamsEqual(
prev?: UseViewModelParameters,
next?: UseViewModelParameters
): boolean {
if (prev === next) return true;
if (!prev || !next) return prev === next;
if ('name' in prev && 'name' in next) {
return prev.name === next.name;
}
if ('useDefault' in prev && 'useDefault' in next) {
return prev.useDefault === next.useDefault;
}
return false;
}
if ('name' in params) {
return 'name' in to && params.name === to.name;
}
if ('useDefault' in params) {
return 'useDefault' in to && params.useDefault === to.useDefault;
}
return false;
};
}
/**
* Custom hook for fetching a view model.
* Hook for fetching a ViewModel from a Rive instance.
*
* @param rive - Rive instance
* @param userParameters - Parameters to load view model
* @returns The ViewModel instance or null if not available
*
* @example
* // Use the default view model
* const viewModel = useViewModel(rive, { useDefault: true });
*
* @example
* // Use a named view model
* const viewModel = useViewModel(rive, { name: 'myViewModel' });
* @param params - Parameters for retrieving a ViewModel
* @param params.rive - The Rive instance to retrieve the ViewModel from
* @param params.name - When provided, specifies the name of the ViewModel to retrieve
* @param params.useDefault - When true, uses the default ViewModel from the Rive instance
* @returns The ViewModel or null if not found
*/
export default function useViewModel(
rive: Rive | null,
userParameters?: UseViewModelParameters
): ViewModel | null {
const [viewModel, setViewModel] = useState<ViewModel | null>(null);
const currentParams = useRef<UseViewModelParameters | null>(null);
export default function useViewModel(params: UseViewModelParameters): ViewModel | null {
const { rive, name, useDefault = false } = params;
const riveRef = useRef<Rive | null>(null);
const paramsRef = useRef<UseViewModelParameters>(params);
const [viewModel, setViewModel] = useState<ViewModel | null>(null);
useEffect(() => {
const parameters = userParameters || defaultParams;
const shouldUpdate = useRef(true);
function getViewModel(): ViewModel | null {
if (rive) {
if ('name' in parameters && parameters.name) {
return rive.viewModelByName(parameters.name);
} else if ('useDefault' in parameters && parameters.useDefault) {
return rive.defaultViewModel();
useEffect(() => {
const isRiveChanged = riveRef.current !== rive;
const areParamsChanged = !areParamsEqual(paramsRef.current, params);
shouldUpdate.current = isRiveChanged || areParamsChanged;
riveRef.current = rive;
paramsRef.current = params;
}, [rive, name, useDefault]);
useEffect(() => {
if (!shouldUpdate.current && viewModel) {
return;
}
}
return null;
}
function setViewModelValue() {
if (!rive) {
setViewModel(null);
currentParams.current = null;
} else {
const viewModel = getViewModel();
setViewModel(viewModel);
currentParams.current = parameters;
}
}
function fetchViewModel() {
const currentRive = riveRef.current;
const currentParams = paramsRef.current;
if (!equal(parameters, currentParams.current)) {
rive?.on(EventType.Load, setViewModelValue);
setViewModelValue();
}
return () => {
rive?.off(EventType.Load, setViewModelValue);
};
}, [rive, userParameters]);
if (!currentRive) {
setViewModel(null);
return;
}
return viewModel;
}
let model: ViewModel | null = null;
if (currentParams && 'name' in currentParams && currentParams.name != null) {
model = currentRive.viewModelByName?.(currentParams.name) || null;
} else if (currentParams && currentParams.useDefault) {
const defaultViewModel = currentRive.defaultViewModel();
if (defaultViewModel) {
model = defaultViewModel;
}
}
setViewModel(model);
shouldUpdate.current = false;
}
fetchViewModel();
const currentRive = riveRef.current;
if (currentRive) {
currentRive.on(EventType.Load, fetchViewModel);
}
return () => {
if (currentRive) {
currentRive.off(EventType.Load, fetchViewModel);
}
};
}, [rive, name, useDefault]);
return viewModel;
}

View File

@@ -1,105 +1,104 @@
import { useState, useEffect, useRef } from 'react';
import {
EventType,
Rive,
ViewModel,
ViewModelInstance,
} from '@rive-app/canvas';
import { ViewModel, ViewModelInstance } from '@rive-app/canvas';
import { UseViewModelInstanceParameters } from '../types';
const defaultParams: UseViewModelInstanceParameters = { useNew: true };
const equal = (
params: UseViewModelInstanceParameters | null,
to: UseViewModelInstanceParameters | null
): boolean => {
if (!params || !to) {
function areParamsEqual(
prev?: UseViewModelInstanceParameters,
next?: UseViewModelInstanceParameters
): boolean {
if (prev === next) return true;
if (!prev || !next) return prev === next;
if ('name' in prev && 'name' in next) {
return prev.name === next.name;
}
if ('useDefault' in prev && 'useDefault' in next) {
return prev.useDefault === next.useDefault;
}
if ('useNew' in prev && 'useNew' in next) {
return prev.useNew === next.useNew;
}
return false;
}
if ('name' in params) {
return 'name' in to && params.name === to.name;
}
if ('useDefault' in params) {
return 'useDefault' in to && params.useDefault === to.useDefault;
}
if ('useNew' in params) {
return 'useNew' in to && params.useNew === to.useNew;
}
return false;
};
}
/**
* Custom hook for fetching a view model instance.
* Hook for fetching a ViewModelInstance from a ViewModel.
*
* @param rive - Rive instance
* @param viewModel - ViewModel to get an instance from
* @param userParameters - Parameters to load view model instance
* @returns The ViewModelInstance or null if not available
*
* @example
* // Create a new instance of the view model
* const viewModelInstance = useViewModelInstance(rive, viewModel, { useNew: true });
*
* @example
* // Use the default instance of the view model
* const viewModelInstance = useViewModelInstance(rive, viewModel, { useDefault: true });
*
* @example
* // Use a named instance of the view model
* const viewModelInstance = useViewModelInstance(rive, viewModel, { name: 'myInstance' });
* @param params - Parameters for retrieving a ViewModelInstance
* @param params.viewModel - The ViewModel to get an instance from
* @param params.name - When provided, specifies the name of the instance to retrieve
* @param params.useDefault - When true, uses the default instance from the ViewModel
* @param params.useNew - When true, creates a new instance of the ViewModel
* @param params.rive - If provided, automatically binds the instance to this Rive instance
* @returns The ViewModelInstance or null if not found
*/
export default function useViewModelInstance(
rive: Rive | null,
viewModel: ViewModel | null,
userParameters?: UseViewModelInstanceParameters
params: UseViewModelInstanceParameters
): ViewModelInstance | null {
const [viewModelInstance, setViewModelInstance] =
useState<ViewModelInstance | null>(null);
const currentParams = useRef<UseViewModelInstanceParameters | null>(null);
const { viewModel, name, useDefault = false, useNew = false, rive } = params;
const [instance, setInstance] = useState<ViewModelInstance | null>(null);
useEffect(() => {
const parameters = userParameters || defaultParams;
const viewModelRef = useRef<ViewModel | null>(viewModel);
const paramsRef = useRef<UseViewModelInstanceParameters>(params);
const instanceRef = useRef<ViewModelInstance | null>(null);
function setInstance(instance: ViewModelInstance | null) {
setViewModelInstance(instance);
rive!.bindViewModelInstance(instance);
currentParams.current = parameters;
}
const shouldUpdate = useRef(true);
function getViewModelInstance(): ViewModelInstance | null {
if (viewModel) {
if ('name' in parameters && parameters.name) {
return viewModel.instanceByName(parameters.name);
} else if ('useDefault' in parameters && parameters.useDefault) {
return viewModel.defaultInstance();
} else if ('useNew' in parameters && parameters.useNew) {
return viewModel.instance();
useEffect(() => {
const isViewModelChanged = viewModelRef.current !== viewModel;
const areParamsChanged = !areParamsEqual(paramsRef.current, params);
shouldUpdate.current = isViewModelChanged || areParamsChanged;
viewModelRef.current = viewModel;
paramsRef.current = params;
}, [viewModel, name, useDefault, useNew]);
useEffect(() => {
if (!shouldUpdate.current && instanceRef.current) {
return;
}
}
return null;
}
function setViewModelValue() {
if (!rive || !viewModel) {
setViewModelInstance(null);
} else {
const instance = getViewModelInstance();
setInstance(instance ?? null);
}
}
const currentViewModel = viewModelRef.current;
const currentParams = paramsRef.current;
if (!equal(parameters, currentParams.current)) {
rive?.on(EventType.Load, setViewModelValue);
setViewModelValue();
}
return () => {
rive?.off(EventType.Load, setViewModelValue);
};
}, [rive, userParameters]);
if (!currentViewModel) {
setInstance(null);
instanceRef.current = null;
return;
}
return viewModelInstance;
}
let result: ViewModelInstance | null = null;
if (currentParams) {
if ('name' in currentParams && currentParams.name != null) {
result = currentViewModel.instanceByName?.(currentParams.name) || null;
} else if ('useDefault' in currentParams && currentParams.useDefault) {
result = currentViewModel.defaultInstance?.() || null;
} else if ('useNew' in currentParams && currentParams.useNew) {
result = currentViewModel.instance?.() || null;
}
} else {
// Default to using default instance if no params provided
result = currentViewModel.defaultInstance?.() || null;
}
instanceRef.current = result;
setInstance(result);
shouldUpdate.current = false;
}, [viewModel, name, useDefault, useNew]);
// Automatically bind to Rive when requested, if not already bound
useEffect(() => {
if (!rive || !instance) return;
if (rive.viewModelInstance !== instance) {
rive.bindViewModelInstance(instance);
}
}, [rive, instance]);
return instance;
}

View File

@@ -1,32 +1,38 @@
import { useCallback } from 'react';
import { ViewModelInstanceBoolean } from '@rive-app/canvas';
import { UseViewModelInstanceBooleanParameters, UseViewModelInstanceBooleanResult } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
/**
* Hook for interacting with boolean ViewModel instance properties.
*
* @param path Path to the property (e.g. "isVisible" or "nested/isVisible")
* @param userParameters Optional parameters including initial value
* @returns Object with value and setter function
*
* @param params - Parameters for interacting with a boolean ViewModel instance property
* @param params.path - The path to the boolean property
* @param params.viewModelInstance - The ViewModelInstance containing the boolean property to operate on
* @returns An object with the boolean value and a setter function
*/
export default function useViewModelInstanceBoolean(
path: string,
userParameters?: UseViewModelInstanceBooleanParameters
params: UseViewModelInstanceBooleanParameters
): UseViewModelInstanceBooleanResult {
return useViewModelInstancePropertyValues<
boolean,
UseViewModelInstanceBooleanParameters,
ViewModelInstanceBoolean,
{ value: boolean; setValue: (value: boolean) => void }
>(
const { path, viewModelInstance } = params;
const result = useViewModelInstanceProperty<ViewModelInstanceBoolean, boolean, Omit<UseViewModelInstanceBooleanResult, 'value'>>(
path,
userParameters,
false,
(instance, name) => instance.boolean(name),
(instance) => instance.value,
(_instance, value, setValue) => ({
value,
setValue
})
viewModelInstance,
{
getProperty: useCallback((vm, p) => vm.boolean(p), []),
getValue: useCallback((prop) => prop.value, []),
defaultValue: null,
buildPropertyOperations: useCallback((safePropertyAccess) => ({
setValue: (newValue: boolean) => {
safePropertyAccess(prop => { prop.value = newValue; });
}
}), [])
}
);
}
return {
value: result.value,
setValue: result.setValue
};
}

View File

@@ -1,43 +1,58 @@
import { useCallback } from 'react';
import { ViewModelInstanceColor } from '@rive-app/canvas';
import { UseViewModelInstanceColorResult, UseViewModelInstanceColorParameters } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
import { UseViewModelInstanceColorParameters, UseViewModelInstanceColorResult } from '../types';
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
/**
* Hook for interacting with color ViewModel instance properties.
*
* @param path Path to the property (e.g. "color" or "nested/color")
* @param userParameters Optional parameters including initial value
* @returns Object with value, setter function, and color utilities
* Hook for interacting with color properties of a ViewModelInstance.
*
* @param params - Parameters for interacting with color properties
* @param params.path - Path to the color property
* @param params.viewModelInstance - The ViewModelInstance containing the color property
* @returns An object with the color value and setter functions for different color formats
*/
export default function useViewModelInstanceColor(
path: string,
userParameters?: UseViewModelInstanceColorParameters
params: UseViewModelInstanceColorParameters
): UseViewModelInstanceColorResult {
return useViewModelInstancePropertyValues<
number,
UseViewModelInstanceColorParameters,
ViewModelInstanceColor,
{
value: number;
setValue: (value: number) => void;
setRgb: (r: number, g: number, b: number) => void;
setRgba: (r: number, g: number, b: number, a: number) => void;
setAlpha: (a: number) => void;
setOpacity: (o: number) => void;
}
>(
const { path, viewModelInstance } = params;
const result = useViewModelInstanceProperty<ViewModelInstanceColor, number, Omit<UseViewModelInstanceColorResult, 'value'>>(
path,
userParameters,
0,
(instance, name) => instance.color(name),
(instance) => instance.value,
(instance, value, setValue) => ({
value,
setValue,
setRgb: (r, g, b) => instance?.rgb(r, g, b),
setRgba: (r, g, b, a) => instance?.rgba(r, g, b, a),
setAlpha: (a) => instance?.alpha(a),
setOpacity: (o) => instance?.opacity(o),
})
viewModelInstance,
{
getProperty: useCallback((vm, p) => vm.color(p), []),
getValue: useCallback((prop) => prop.value, []),
defaultValue: null,
buildPropertyOperations: useCallback((safePropertyAccess) => ({
setValue: (newValue: number) => {
safePropertyAccess(prop => { prop.value = newValue; });
},
setRgb: (r: number, g: number, b: number) => {
safePropertyAccess(prop => { prop.rgb(r, g, b); });
},
setRgba: (r: number, g: number, b: number, a: number) => {
safePropertyAccess(prop => { prop.rgba(r, g, b, a); });
},
setAlpha: (a: number) => {
safePropertyAccess(prop => { prop.alpha(a); });
},
setOpacity: (o: number) => {
safePropertyAccess(prop => { prop.opacity(o); });
}
}), [])
}
);
}
return {
value: result.value,
setValue: result.setValue,
setRgb: result.setRgb,
setRgba: result.setRgba,
setAlpha: result.setAlpha,
setOpacity: result.setOpacity
};
}

View File

@@ -1,37 +1,45 @@
import { useCallback } from 'react';
import { ViewModelInstanceEnum } from '@rive-app/canvas';
import { UseViewModelInstanceEnumParameters, UseViewModelInstanceEnumResult } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
/**
* Hook for interacting with enum ViewModel instance properties.
*
* @param path Path to the property (e.g. "state" or "nested/state")
* @param userParameters Optional parameters including initial value
* @returns Object with value, values array, and setter function
* Hook for interacting with enum properties of a ViewModelInstance.
*
* @param params - Parameters for interacting with enum properties
* @param params.path - Path to the enum property (e.g. "state" or "group/state")
* @param params.viewModelInstance - The ViewModelInstance containing the enum property
* @returns An object with the enum value, available values, and a setter function
*/
export default function useViewModelInstanceEnum(
path: string,
userParameters?: UseViewModelInstanceEnumParameters
params: UseViewModelInstanceEnumParameters
): UseViewModelInstanceEnumResult {
return useViewModelInstancePropertyValues<
string,
UseViewModelInstanceEnumParameters,
const { path, viewModelInstance } = params;
const result = useViewModelInstanceProperty<
ViewModelInstanceEnum,
{
value: string;
setValue: (value: string) => void;
values: string[];
}
string,
Omit<UseViewModelInstanceEnumResult, 'value' | 'values'>,
string[]
>(
path,
userParameters,
'',
(instance, name) => instance.enum(name),
(instance) => instance.value,
(instance, value, setValue) => ({
value,
setValue,
values: instance?.values || []
})
viewModelInstance,
{
getProperty: useCallback((vm, p) => vm.enum(p), []),
getValue: useCallback((prop) => prop.value, []),
defaultValue: null,
getExtendedData: useCallback((prop) => prop.values, []),
buildPropertyOperations: useCallback((safePropertyAccess) => ({
setValue: (newValue: string) => {
safePropertyAccess(prop => { prop.value = newValue; });
}
}), [])
}
);
}
return {
value: result.value,
values: result.extendedData || [],
setValue: result.setValue
};
}

View File

@@ -1,32 +1,38 @@
import { useCallback } from 'react';
import { ViewModelInstanceNumber } from '@rive-app/canvas';
import { UseViewModelInstanceNumberParameters, UseViewModelInstanceNumberResult } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
/**
* Hook for interacting with numeric ViewModel instance properties.
*
* @param path Path to the property (e.g. "itemCount" or "nested/itemCount")
* @param userParameters Optional parameters including initial value
* @returns Object with value and setter function
* Hook for interacting with number properties of a ViewModelInstance.
*
* @param params - Parameters for interacting with number properties
* @param params.path - Path to the number property (e.g. "speed" or "group/speed")
* @param params.viewModelInstance - The ViewModelInstance containing the number property
* @returns An object with the number value and a setter function
*/
export default function useViewModelInstanceNumber(
path: string,
userParameters?: UseViewModelInstanceNumberParameters
params: UseViewModelInstanceNumberParameters
): UseViewModelInstanceNumberResult {
return useViewModelInstancePropertyValues<
number,
UseViewModelInstanceNumberParameters,
ViewModelInstanceNumber,
{ value: number; setValue: (value: number) => void }
>(
path,
userParameters,
0,
(instance, name) => instance.number(name),
(instance) => instance.value,
(_instance, value, setValue) => ({
value,
setValue
})
);
}
const { path, viewModelInstance } = params;
const result = useViewModelInstanceProperty<ViewModelInstanceNumber, number, Omit<UseViewModelInstanceNumberResult, 'value'>>(
path,
viewModelInstance,
{
getProperty: useCallback((vm, p) => vm.number(p), []),
getValue: useCallback((prop) => prop.value, []),
defaultValue: null,
buildPropertyOperations: useCallback((safePropertyAccess) => ({
setValue: (newValue: number) => {
safePropertyAccess(prop => { prop.value = newValue; });
}
}), [])
}
);
return {
value: result.value,
setValue: result.setValue
};
}

View File

@@ -1,99 +1,172 @@
import { useState, useEffect, useRef } from 'react';
import {
EventType,
ViewModelInstance,
} from '@rive-app/canvas';
import { UseViewModelInstanceValueParameters } from '../types';
const defaultParams: UseViewModelInstanceValueParameters = {
viewModelInstance: null,
};
const equal = (
path: string[],
params: UseViewModelInstanceValueParameters | null,
to: HookArguments | null
): boolean => {
if (!params || !to) {
return false;
}
if (
params.rive !== to.parameters.rive ||
params.viewModelInstance !== to.parameters.viewModelInstance ||
path.join('') !== to.path.join('')
) {
return false;
}
return true;
};
type HookArguments = {
path: string[],
parameters: UseViewModelInstanceValueParameters,
}
import { useState, useEffect, useCallback, useRef } from 'react';
import { ViewModelInstance, ViewModelInstanceValue } from '@rive-app/canvas';
/**
* Custom hook for fetching a view model instance value.
*
* @param name - name of the propery
* @param path - Path to reach the required property
* @param userParameters - Parameters to load view model instance number
* @returns
* Base hook for all ViewModelInstance property interactions.
*
* This hook handles the common tasks needed when working with Rive properties:
* 1. Safely accessing properties (even during hot-reload)
* 2. Keeping React state in sync with property changes
* 3. Providing type safety for all operations
*
* @param path - Property path in the ViewModelInstance
* @param viewModelInstance - The source ViewModelInstance
* @param options - Configuration for working with the property
* @returns Object with the value and operations
*/
export default function useViewModelInstanceProperty(
path: string[] = [],
userParameters?: UseViewModelInstanceValueParameters
): ViewModelInstance | null {
const [viewModelInstance, setViewModelValue] =
useState<ViewModelInstance | null>(null);
const currentArguments = useRef<HookArguments | null>(
null
);
export function useViewModelInstanceProperty<P extends ViewModelInstanceValue, V, R, E = undefined>(
path: string,
viewModelInstance: ViewModelInstance | null | undefined,
options: {
/** Function to get the property from a ViewModelInstance */
getProperty: (vm: ViewModelInstance, path: string) => P | null;
useEffect(() => {
const parameters = {
...defaultParams,
...userParameters,
};
/** Function to get the current value from the property */
getValue: (prop: P) => V;
function getInstanceValue(): ViewModelInstance | null {
let viewModelInstance: ViewModelInstance | null = null;
if (userParameters?.viewModelInstance) {
viewModelInstance = userParameters?.viewModelInstance;
} else if (userParameters?.rive) {
viewModelInstance = userParameters?.rive?.viewModelInstance;
}
if (viewModelInstance) {
let index = 0;
while (index < path?.length) {
if (!viewModelInstance) {
return null;
}
viewModelInstance = viewModelInstance?.viewModel(path[index]);
index++;
/** Default value to use when property is unavailable */
defaultValue: V | null;
/**
* Function to create the property-specific operations
*
* @param safePropertyAccess - Helper function for safely working with properties. Handles stale property references.
* @returns Object with operations like setValue, trigger, etc.
*/
buildPropertyOperations: (safePropertyAccess: (callback: (prop: P) => void) => void) => R;
/** Optional callback for property events (mainly used by triggers) */
onPropertyEvent?: () => void;
/**
* Optional function to extract additional property data (like enum values)
* Returns undefined if not provided
*/
getExtendedData?: (prop: P) => E;
}
): R & { value: V | null } & (E extends undefined ? {} : { extendedData: E | null }) {
const [property, setProperty] = useState<P | null>(null);
const [value, setValue] = useState<V | null>(options.defaultValue);
const [extendedData, setExtendedData] = useState<E | null>(null);
const instanceRef = useRef<ViewModelInstance | null | undefined>(null);
const pathRef = useRef<string>(path);
const optionsRef = useRef(options);
useEffect(() => {
optionsRef.current = options;
}, [options]);
const updateProperty = useCallback(() => {
const currentInstance = instanceRef.current;
const currentPath = pathRef.current;
const currentOptions = optionsRef.current;
if (!currentInstance || !currentPath) {
setProperty(null);
setValue(currentOptions.defaultValue);
setExtendedData(null);
return () => { };
}
return viewModelInstance;
}
return null;
const prop = currentOptions.getProperty(currentInstance, currentPath);
if (prop) {
setProperty(prop);
setValue(currentOptions.getValue(prop));
if (currentOptions.getExtendedData) {
setExtendedData(currentOptions.getExtendedData(prop));
}
const handleChange = () => {
setValue(currentOptions.getValue(prop));
if (currentOptions.getExtendedData) {
setExtendedData(currentOptions.getExtendedData(prop));
}
if (currentOptions.onPropertyEvent) {
currentOptions.onPropertyEvent();
}
};
prop.on(handleChange);
return () => {
prop.off(handleChange);
};
}
return () => { };
}, []);
useEffect(() => {
instanceRef.current = viewModelInstance;
pathRef.current = path;
// subscribe & get our unsubscribe function
const cleanup = updateProperty();
return cleanup;
}, [viewModelInstance, path, updateProperty]);
/**
* Helper function that safely accesses properties, even during hot-reload.
*
* It tries to:
* 1. Use the existing property reference when possible
* 2. Fetch a fresh reference when needed
* 3. Apply the callback to whichever reference works
*/
const safePropertyAccess = useCallback(
(callback: (prop: P) => void) => {
// Try the fast path first
if (property && instanceRef.current === viewModelInstance) {
try {
callback(property);
// Update extended data after callback if available
if (optionsRef.current.getExtendedData) {
setExtendedData(optionsRef.current.getExtendedData(property));
}
return;
} catch (e) {
// Property might be stale - so we silently catch and try alternative
// This commonly happens during hot module replacement
}
}
// Get a fresh property if needed
if (instanceRef.current) {
try {
const freshProp = optionsRef.current.getProperty(instanceRef.current, pathRef.current);
if (freshProp) {
setProperty(freshProp);
callback(freshProp);
// Update extended data after callback if available
if (optionsRef.current.getExtendedData) {
setExtendedData(optionsRef.current.getExtendedData(freshProp));
}
}
} catch (e) {
// Silently fail during hot-reload - this is expected behavior
// We don't want to crash the app during development
}
}
},
[property, viewModelInstance]
);
const operations = options.buildPropertyOperations(safePropertyAccess);
const result = {
value,
...operations
} as R & { value: V | null } & (E extends undefined ? {} : { extendedData: E | null });
if (options.getExtendedData) {
(result as any).extendedData = extendedData;
}
function searchViewModelInstance() {
const instanceValue = getInstanceValue();
setViewModelValue(instanceValue);
currentArguments.current = {
parameters,
path,
};
}
if (!equal(path, parameters, currentArguments.current)) {
parameters.rive?.on(EventType.Load, searchViewModelInstance);
searchViewModelInstance();
}
return () => {
parameters.rive?.off(EventType.Load, searchViewModelInstance);
};
}, [path, userParameters]);
return viewModelInstance;
}
return result;
}

View File

@@ -1,110 +0,0 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { EventCallback, EventType, ViewModelInstance, ViewModelInstanceValue } from '@rive-app/canvas';
import { UseViewModelInstancePropertyType } from '../types';
import useViewModelInstanceProperty from './useViewModelInstanceProperty';
export function useViewModelInstancePropertyValues<
T extends unknown,
P extends UseViewModelInstancePropertyType,
V extends ViewModelInstanceValue,
R extends Record<string, any>
>(
path: string,
userParameters: P | undefined,
defaultValue: T,
propertyGetter: (instance: ViewModelInstance, name: string) => V | null,
valueGetter: (propertyInstance: V) => T,
resultBuilder: (
propertyInstance: V | null,
value: T,
setValue: (value: T) => void
) => R
): R {
const [propertyInstance, setPropertyInstance] = useState<V | null>(null);
const [value, setValueState] = useState<T>(
(userParameters as any)?.initialValue ?? defaultValue
);
const pathSegments = path.includes('/') ? path.split('/') : [];
const propertyName = path.includes('/') ? path.split('/').pop() || path : path;
const basePath = pathSegments.length > 0 ? pathSegments.slice(0, -1) : [];
const viewModelInstance = useViewModelInstanceProperty(basePath, userParameters);
// Track current arguments to prevent unnecessary updates
const currentArgs = useRef<{
path: string,
parameters: P | undefined,
viewModelInstance: ViewModelInstance | null
} | null>(null);
useEffect(() => {
function searchProperty() {
if (!viewModelInstance) {
setPropertyInstance(null);
return;
}
const instance = propertyGetter(viewModelInstance, propertyName);
if (instance !== null) {
if ((userParameters as any)?.initialValue !== undefined) {
(instance as any).value = (userParameters as any).initialValue;
}
setValueState(valueGetter(instance));
setPropertyInstance(instance);
currentArgs.current = {
parameters: userParameters,
path,
viewModelInstance,
};
}
}
const argsChanged = !currentArgs.current ||
currentArgs.current.path !== path ||
currentArgs.current.viewModelInstance !== viewModelInstance;
if (argsChanged) {
userParameters?.rive?.on(EventType.Load, searchProperty);
searchProperty();
}
return () => {
userParameters?.rive?.off(EventType.Load, searchProperty);
};
}, [path, userParameters, viewModelInstance, propertyName, valueGetter]);
// We subscribe to value changes by default with the property hooks.
useEffect(() => {
if (!propertyInstance) return;
const handleChange: EventCallback = (event) => {
setValueState(event as unknown as T);
};
propertyInstance.on(handleChange);
return () => {
propertyInstance.off(handleChange);
};
}, [propertyInstance]);
const setValue = useCallback((newValue: T) => {
if (propertyInstance) {
(propertyInstance as any).value = newValue;
} else {
// If no instance yet, just update React state
setValueState(newValue);
}
}, [propertyInstance]);
return resultBuilder(propertyInstance, value, setValue);
}

View File

@@ -1,32 +1,38 @@
import { useCallback } from 'react';
import { ViewModelInstanceString } from '@rive-app/canvas';
import { UseViewModelInstanceStringParameters, UseViewModelInstanceStringResult } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
/**
* Hook for interacting with string ViewModel instance properties.
*
* @param path Path to the property (e.g. "text" or "nested/text")
* @param userParameters Optional parameters including initial value
* @returns Object with value and setter function
* Hook for interacting with string properties of a ViewModelInstance.
*
* @param params - Parameters for interacting with string properties
* @param params.path - Path to the property (e.g. "text" or "nested/text")
* @param params.viewModelInstance - The ViewModelInstance containing the string property
* @returns An object with the string value and a setter function
*/
export default function useViewModelInstanceString(
path: string,
userParameters?: UseViewModelInstanceStringParameters
params: UseViewModelInstanceStringParameters
): UseViewModelInstanceStringResult {
return useViewModelInstancePropertyValues<
string,
UseViewModelInstanceStringParameters,
ViewModelInstanceString,
{ value: string; setValue: (value: string) => void }
>(
const { path, viewModelInstance } = params;
const result = useViewModelInstanceProperty<ViewModelInstanceString, string, Omit<UseViewModelInstanceStringResult, 'value'>>(
path,
userParameters,
'',
(instance, name) => instance.string(name),
(instance) => instance.value,
(_instance, value, setValue) => ({
value,
setValue
})
viewModelInstance,
{
getProperty: useCallback((vm, p) => vm.string(p), []),
getValue: useCallback((prop) => prop.value, []),
defaultValue: null,
buildPropertyOperations: useCallback((safePropertyAccess) => ({
setValue: (newValue: string) => {
safePropertyAccess(prop => { prop.value = newValue; });
}
}), [])
}
);
}
return {
value: result.value,
setValue: result.setValue
};
}

View File

@@ -1,58 +1,40 @@
import { useEffect, useCallback } from 'react';
import { useCallback } from 'react';
import { ViewModelInstanceTrigger } from '@rive-app/canvas';
import { UseViewModelInstanceTriggerParameters, UseViewModelInstanceTriggerResult } from '../types';
import { useViewModelInstancePropertyValues } from './useViewModelInstancePropertyValues';
import { useViewModelInstanceProperty } from './useViewModelInstanceProperty';
/**
* Hook for interacting with trigger ViewModel instance properties.
*
* @param path Path to the property (e.g. "buttonPress" or "nested/buttonPress")
* @param userParameters Optional parameters including onTrigger callback
* @returns Object with trigger function
* Hook for interacting with trigger properties of a ViewModelInstance.
*
* @param params - Parameters for interacting with trigger properties
* @param params.path - Path to the trigger property (e.g. "onTap" or "group/onTap")
* @param params.viewModelInstance - The ViewModelInstance containing the trigger property
* @param params.onTrigger - Callback that runs when the trigger is fired
* @returns An object with a trigger function
*/
export default function useViewModelInstanceTrigger(
path: string,
userParameters?: UseViewModelInstanceTriggerParameters
params: UseViewModelInstanceTriggerParameters
): UseViewModelInstanceTriggerResult {
const result = useViewModelInstancePropertyValues<
void,
UseViewModelInstanceTriggerParameters,
ViewModelInstanceTrigger,
{
trigger: () => void;
instance: ViewModelInstanceTrigger | null;
}
>(
const { path, viewModelInstance, onTrigger } = params;
const { trigger } = useViewModelInstanceProperty<ViewModelInstanceTrigger, undefined, UseViewModelInstanceTriggerResult>(
path,
userParameters,
undefined,
(instance, name) => instance.trigger(name),
() => undefined,
(instance) => ({
trigger: () => {
instance?.trigger();
},
instance
})
viewModelInstance,
{
getProperty: useCallback((vm, p) => vm.trigger(p), []),
getValue: useCallback(() => undefined, []), // Triggers don't have a 'value'
defaultValue: null,
onPropertyEvent: onTrigger,
buildPropertyOperations: useCallback((safePropertyAccess) => ({
trigger: () => {
safePropertyAccess(prop => {
prop.trigger();
});
}
}), [])
}
);
const { instance } = result;
useEffect(() => {
if (instance && userParameters?.onTrigger) {
instance.on(userParameters.onTrigger);
return () => {
instance.off(userParameters.onTrigger);
};
}
}, [instance, userParameters?.onTrigger]);
const trigger = useCallback(() => {
instance?.trigger();
}, [instance]);
return {
trigger
};
}
return { trigger };
}

View File

@@ -1,173 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import {
EventType,
ViewModelInstance,
ViewModelInstanceValue,
} from '@rive-app/canvas';
import { UseViewModelInstanceValueParameters } from '../types';
const defaultParams: UseViewModelInstanceValueParameters = {
viewModelInstance: null,
};
const equal = (
properties: string[],
params: UseViewModelInstanceValueParameters | null,
to: HookArguments | null
): boolean => {
if (!params || !to) {
return false;
}
if (properties.length !== to.properties.length) {
return false;
}
for (let i = 0; i < properties.length; i += 1) {
if (properties[i] !== to.properties[i]) {
return false;
}
}
if (
params.rive !== to.parameters.rive ||
params.viewModelInstance !== to.parameters.viewModelInstance
) {
return false;
}
return true;
};
type HookArguments = {
properties: string[];
parameters: UseViewModelInstanceValueParameters;
};
type PropertyResult = {
query: string;
property: ViewModelInstanceValue | null;
};
/**
* Custom hook for fetching a view model instance value.
*
* @param properties - list of queries properties
* @param path - Path to reach the required property
* @param userParameters - Parameters to load view model properties
* @returns
*/
export default function useViewModelProperties(
properties: string[],
userParameters?: UseViewModelInstanceValueParameters
): PropertyResult[] {
const [result, setResult] = useState<PropertyResult[]>([]);
const currentArguments = useRef<HookArguments | null>(null);
useEffect(() => {
const parameters = {
...defaultParams,
...userParameters,
};
function getViewModelInstance() {
if (parameters.viewModelInstance) {
return parameters.viewModelInstance;
} else if (parameters.rive) {
return parameters.rive.viewModelInstance;
}
return null;
}
function getPropertyViewModelInstance(
path: string
): ViewModelInstance | null {
const viewModelInstance: ViewModelInstance | null = getViewModelInstance();
if (path === '') {
return viewModelInstance;
}
return viewModelInstance?.viewModel(path) || null;
}
function getProperty(
viewModelInstance: ViewModelInstance | null,
name: string
): ViewModelInstanceValue | null {
if (viewModelInstance) {
const viewModelProperties = viewModelInstance.properties;
const propertyData = viewModelProperties.find((candidate) => {
if (candidate.name === name) {
return candidate;
}
});
if (propertyData !== null) {
switch (propertyData!.type.toString()) {
case 'number':
return viewModelInstance.number(name);
case 'string':
return viewModelInstance.string(name);
case 'boolean':
return viewModelInstance.boolean(name);
case 'enumType':
return viewModelInstance.enum(name);
case 'color':
return viewModelInstance.color(name);
case 'trigger':
return viewModelInstance.trigger(name);
}
}
}
return null;
}
function searchViewModelValues() {
const viewModelInstance = getViewModelInstance();
if (!viewModelInstance) {
setResult([]);
} else {
const result: PropertyResult[] = [];
properties.forEach((propertyQuery) => {
if (propertyQuery === '') {
result.push({
query: propertyQuery,
property: null,
});
} else {
const propertyParts = propertyQuery.split('/');
const propertyName = propertyParts.pop();
const propertyViewModelPath = propertyParts.join('/');
const propertyViewModelInstance = getPropertyViewModelInstance(
propertyViewModelPath
);
const property = getProperty(
propertyViewModelInstance,
propertyName!
);
if (property) {
result.push({
query: propertyQuery,
property: property,
});
} else {
result.push({
query: propertyQuery,
property: null,
});
}
}
});
setResult(result);
}
currentArguments.current = {
properties: properties,
parameters: parameters,
};
}
if (!equal(properties, parameters, currentArguments.current)) {
parameters.rive?.on(EventType.Load, searchViewModelValues);
searchViewModelValues();
}
return () => {
parameters.rive?.off(EventType.Load, searchViewModelValues);
};
}, [name, userParameters]);
return result;
}

View File

@@ -1,112 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import {
EventType,
ViewModelInstance,
} from '@rive-app/canvas';
import {
UseViewModelInstancePropertyType,
AcceptedVieModelType,
} from '../types';
import useViewModelInstanceProperty from './useViewModelInstanceProperty';
import { DataType } from '@rive-app/canvas/rive_advanced.mjs';
const defaultParams: UseViewModelInstancePropertyType = {
viewModelInstance: null,
};
const equal = <U extends UseViewModelInstancePropertyType>(
name: string,
params: U | null,
viewModelInstance: ViewModelInstance | null,
to: HookArguments | null
): boolean => {
if (!params || !to) {
return false;
}
if (
params.initialValue !== to.parameters.initialValue ||
name !== to.name ||
viewModelInstance !== to.viewModelInstance
) {
return false;
}
return true;
};
type HookArguments = {
name: string;
parameters: UseViewModelInstancePropertyType;
viewModelInstance: ViewModelInstance | null;
};
/**
* Custom hook for fetching a view model instance value.
*
* @param name - name of the propery
* @param path - Path to reach the required property
* @param userParameters - Parameters to load view model instance number
* @returns
*/
export default function useViewModelProperty<
T extends UseViewModelInstancePropertyType,
U extends AcceptedVieModelType<T>
>(name: string, path: string[] = [], userParameters?: T): U | null {
const [viewModel, setViewModelValue] = useState<AcceptedVieModelType<T> | null>(null);
const currentArguments = useRef<HookArguments | null>(null);
const viewModelInstance = useViewModelInstanceProperty(path, userParameters);
useEffect(() => {
const parameters: T = {
...defaultParams,
...(userParameters as T),
};
function getVMI(name: string): U | null {
const properties = viewModelInstance!.properties;
const propData = properties.find((value) => value.name === name);
if (propData === null) {
return null;
}
if (propData!.type === DataType.number) {
return viewModelInstance!.number(name) as U;
} else if (propData!.type === DataType.string) {
return viewModelInstance!.string(name) as U;
} else if (propData!.type === DataType.boolean) {
return viewModelInstance!.boolean(name) as U;
} else if (propData!.type === DataType.color) {
return viewModelInstance!.color(name) as U;
}
return null;
}
function setInitialValue(property: U, params: T) {
if (params.initialValue !== undefined) {
property.value = params.initialValue;
}
}
function searchViewModelValue() {
const instanceValue = getVMI(name);
if (instanceValue !== null) {
setInitialValue(instanceValue, parameters);
}
setViewModelValue(instanceValue);
currentArguments.current = {
parameters,
name,
viewModelInstance,
};
}
if (!equal(name, parameters, viewModelInstance, currentArguments.current)) {
parameters.rive?.on(EventType.Load, searchViewModelValue);
searchViewModelValue();
}
return () => {
parameters.rive?.off(EventType.Load, searchViewModelValue);
};
}, [name, userParameters, viewModelInstance]);
return viewModel as U;
}

View File

@@ -9,31 +9,29 @@ import useViewModelInstanceBoolean from './hooks/useViewModelInstanceBoolean';
import useViewModelInstanceColor from './hooks/useViewModelInstanceColor';
import useViewModelInstanceEnum from './hooks/useViewModelInstanceEnum';
import useViewModelInstanceTrigger from './hooks/useViewModelInstanceTrigger';
import useViewModelProperties from './hooks/useViewModelProperties';
import useResizeCanvas from './hooks/useResizeCanvas';
import useRiveFile from './hooks/useRiveFile';
export default Rive;
export {
useRive,
useStateMachineInput,
useResizeCanvas,
useRiveFile,
useViewModel,
useViewModelInstance,
useViewModelInstanceNumber,
useViewModelInstanceString,
useViewModelInstanceBoolean,
useViewModelInstanceColor,
useViewModelInstanceEnum,
useViewModelInstanceTrigger,
useViewModelProperties,
RiveProps,
useRive,
useStateMachineInput,
useResizeCanvas,
useRiveFile,
useViewModel,
useViewModelInstance,
useViewModelInstanceNumber,
useViewModelInstanceString,
useViewModelInstanceBoolean,
useViewModelInstanceColor,
useViewModelInstanceEnum,
useViewModelInstanceTrigger,
RiveProps,
};
export {
RiveState,
UseRiveParameters,
UseRiveFileParameters,
UseRiveOptions,
RiveState,
UseRiveParameters,
UseRiveFileParameters,
UseRiveOptions,
} from './types';
export * from '@rive-app/canvas';

View File

@@ -1,13 +1,10 @@
import {
Rive,
type ViewModel,
RiveFile,
RiveFileParameters,
RiveParameters,
ViewModelInstance,
ViewModelInstanceBoolean,
ViewModelInstanceNumber,
ViewModelInstanceString,
ViewModelInstanceColor,
type ViewModelInstance,
} from '@rive-app/canvas';
import { ComponentProps, RefCallback } from 'react';
@@ -63,78 +60,104 @@ export type RiveFileState = {
status: FileStatus;
};
export type UseViewModelParameters = {
useDefault?: boolean;
name?: string;
};
/**
* Parameters for retrieving a ViewModel from a Rive instance.
*
* @property rive - The Rive instance to retrieve the ViewModel from.
* @property name - When provided, specifies the name of the ViewModel to retrieve.
* @property useDefault - When true, uses the default ViewModel from the Rive instance.
*/
export type UseViewModelParameters =
| { rive: Rive | null; name: string; useDefault?: never }
| { rive: Rive | null; useDefault?: boolean; name?: never };
export type UseViewModelInstanceParameters = {
useNew?: boolean;
useDefault?: boolean;
name?: string;
};
/**
* Parameters for retrieving a ViewModelInstance.
*
* @property viewModel - The ViewModel to get an instance from.
* @property name - When provided, specifies the name of the instance to retrieve.
* @property useDefault - When true, uses the default instance from the ViewModel.
* @property useNew - When true, creates a new instance of the ViewModel.
* @property rive - When provided, automatically binds the instance to this Rive instance.
*/
export type UseViewModelInstanceParameters =
| { viewModel: ViewModel | null; name: string; rive?: Rive | null; useDefault?: never; useNew?: never }
| { viewModel: ViewModel | null; useDefault?: boolean; rive?: Rive | null; name?: never; useNew?: never }
| { viewModel: ViewModel | null; useNew?: boolean; rive?: Rive | null; name?: never; useDefault?: never };
export type UseViewModelInstanceValueParameters = {
viewModelInstance?: ViewModelInstance | null;
rive?: Rive | null;
};
export type UseViewModelInstanceNumberParameters =
UseViewModelInstanceValueParameters & {
initialValue?: number;
};
/**
* Parameters for interacting with number properties of a ViewModelInstance
* @property path - Path to the number property (e.g. "speed" or "group/speed")
* @property viewModelInstance - The ViewModelInstance containing the number property
*/
export type UseViewModelInstanceNumberParameters = {
path: string;
viewModelInstance?: ViewModelInstance | null;
};
export type UseViewModelInstanceStringParameters =
UseViewModelInstanceValueParameters & {
initialValue?: string;
};
/**
* Parameters for interacting with string properties of a ViewModelInstance
* @property path - Path to the string property (e.g. "text" or "nested/text")
* @property viewModelInstance - The ViewModelInstance containing the string property
*/
export type UseViewModelInstanceStringParameters = {
path: string;
viewModelInstance?: ViewModelInstance | null;
};
export type UseViewModelInstanceBooleanParameters =
UseViewModelInstanceValueParameters & {
initialValue?: boolean;
};
/**
* Parameters for interacting with boolean properties of a ViewModelInstance
* @property path - Path to the boolean property (e.g. "agreedToTerms" or "group/agreedToTerms")
* @property viewModelInstance - The ViewModelInstance containing the boolean property
*/
export type UseViewModelInstanceBooleanParameters = {
path: string;
viewModelInstance?: ViewModelInstance | null;
};
export type UseViewModelInstanceColorParameters =
UseViewModelInstanceValueParameters & {
initialValue?: number;
};
/**
* Parameters for interacting with color properties of a ViewModelInstance
* @property path - Path to the color property (e.g. "color" or "group/color")
* @property viewModelInstance - The ViewModelInstance containing the color property
*/
export type UseViewModelInstanceColorParameters = {
path: string;
viewModelInstance?: ViewModelInstance | null;
};
export type UseViewModelInstanceEnumParameters =
UseViewModelInstanceValueParameters & {
initialValue?: string;
};
/**
* Parameters for interacting with enum properties of a ViewModelInstance
* @property path - Path to the enum property (e.g. "state" or "group/state")
* @property viewModelInstance - The ViewModelInstance containing the enum property
*/
export type UseViewModelInstanceEnumParameters = {
path: string;
viewModelInstance?: ViewModelInstance | null;
};
export type UseViewModelInstanceTriggerParameters = UseViewModelInstanceValueParameters & {
/**
* Callback that runs when the trigger is fired.
*/
/**
* Parameters for interacting with trigger properties of a ViewModelInstance
* @property path - Path to the trigger property (e.g. "onTap" or "group/onTap")
* @property viewModelInstance - The ViewModelInstance containing the trigger
* @property onTrigger - Callback that runs when the trigger fires
*/
export type UseViewModelInstanceTriggerParameters = {
path: string;
viewModelInstance?: ViewModelInstance | null;
onTrigger?: () => void;
};
export type UseViewModelInstancePropertyType =
| UseViewModelInstanceNumberParameters
| UseViewModelInstanceStringParameters
| UseViewModelInstanceBooleanParameters
| UseViewModelInstanceColorParameters
| UseViewModelInstanceEnumParameters;
export type AcceptedVieModelType<T> =
T extends UseViewModelInstanceNumberParameters
? ViewModelInstanceNumber
: T extends UseViewModelInstanceStringParameters
? ViewModelInstanceString
: T extends UseViewModelInstanceBooleanParameters
? ViewModelInstanceBoolean
: T extends UseViewModelInstanceColorParameters
? ViewModelInstanceColor
: never;
export type UseViewModelInstanceNumberResult = {
/**
* The current value of the number.
*/
value: number;
value: number | null;
/**
* Set the value of the number.
* @param value - The value to set the number to.
@@ -145,7 +168,7 @@ export type UseViewModelInstanceStringResult = {
/**
* The current value of the string.
*/
value: string;
value: string | null;
/**
* Set the value of the string.
* @param value - The value to set the string to.
@@ -156,7 +179,7 @@ export type UseViewModelInstanceBooleanResult = {
/**
* The current value of the boolean.
*/
value: boolean;
value: boolean | null;
/**
* Set the value of the boolean.
* @param value - The value to set the boolean to.
@@ -168,7 +191,7 @@ export type UseViewModelInstanceColorResult = {
/**
* The current value of the color.
*/
value: number;
value: number | null;
/**
* Set the value of the color.
* @param value - The value to set the color to.
@@ -203,7 +226,7 @@ export type UseViewModelInstanceEnumResult = {
/**
* The current value of the enum.
*/
value: string;
value: string | null;
/**
* Set the value of the enum.
* @param value - The value to set the enum to.