-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Why
-
Stable
riveViewRef— Upstream usesuseStatefor the view, so when it becomes ready we get a new value → re-renders and effects run again. We need a ref so we can callriveViewRef.current?.playIfNeeded()in effects without the ref changing and causing double runs or flicker. -
onViewReady— Gives us a one-time callback when the view is ready (e.g. foruseViewModelInstance). The callback is stored in a ref so the ref setter stays stable. -
Stable
setPropertyValue— Upstream’s setter has[property, path]in its deps, so we get a new function when those change. We use the setter in effects; a changing setter re-runs effects and can double-fire triggers. The patch uses refs so the setter has[]deps and stays stable. -
useViewModelInstanceacceptsRefObject— We want to pass the sameriveViewRefwe use for playback intouseViewModelInstance. Upstream only takes a concreteRiveViewRef; the patch addsRefObject<RiveViewRef | null>and unwrapssource.current.
Sample Patch
diff --git a/src/hooks/useRive.ts b/src/hooks/useRive.ts
index 078a77eeda7122f92a9d71a31db4890b808ec32d..601966848bf6c142ebadcef2ce01aa02cb5a0eb8 100644
--- a/src/hooks/useRive.ts
+++ b/src/hooks/useRive.ts
@@ -1,10 +1,17 @@
-import { useRef, useCallback, useState } from 'react';
+import { useRef, useCallback } from 'react';
import type { RiveViewRef } from '@rive-app/react-native';
-export function useRive() {
+export interface UseRiveOptions {
+ /** Called when the Rive view is ready. Use this to e.g. set state and trigger a re-render if you need useViewModelInstance to see the ref. */
+ onViewReady?: (viewRef: RiveViewRef) => void;
+}
+
+export function useRive(options?: UseRiveOptions) {
const riveRef = useRef<RiveViewRef>(null);
- const [riveViewRef, setRiveViewRef] = useState<RiveViewRef | null>(null);
+ const riveViewRef = useRef<RiveViewRef | null>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+ const onViewReadyRef = useRef(options?.onViewReady);
+ onViewReadyRef.current = options?.onViewReady;
const setRef = useCallback((node: RiveViewRef | null) => {
if (riveRef.current !== node) {
@@ -25,16 +32,17 @@ export function useRive() {
// or add this timeout natively and return false
Promise.race([node?.awaitViewReady(), timeout])
.then((result) => {
- if (result === true) {
- setRiveViewRef(node);
+ if (result === true && node) {
+ riveViewRef.current = node;
+ onViewReadyRef.current?.(node);
} else {
console.warn('Rive view ready check returned false');
- setRiveViewRef(null);
+ riveViewRef.current = null;
}
})
.catch((error) => {
console.warn('Failed to initialize Rive view:', error);
- setRiveViewRef(null);
+ riveViewRef.current = null;
})
.finally(() => {
// Clear the timeout in both success and error cases
diff --git a/src/hooks/useRiveProperty.ts b/src/hooks/useRiveProperty.ts
index a49bdfe9bf8aa5302690b3e5ef81de415166bf85..f3b1c774fbce1fc23c5397b59cb5055027a05880 100644
--- a/src/hooks/useRiveProperty.ts
+++ b/src/hooks/useRiveProperty.ts
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useState, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
type ObservableProperty,
type ViewModelInstance,
@@ -46,6 +46,10 @@ export function useRiveProperty<P extends ViewModelProperty, T>(
// Initialize state with property's current value (if available)
const [value, setValue] = useState<T | undefined>(() => property?.value);
const [error, setError] = useState<Error | null>(null);
+ const propertyRef = useRef(property);
+ const pathRef = useRef(path);
+ propertyRef.current = property;
+ pathRef.current = path;
// Sync value when property reference changes (path or instance changed)
useEffect(() => {
@@ -86,26 +90,26 @@ export function useRiveProperty<P extends ViewModelProperty, T>(
};
}, [options, property]);
- // Set the value of the property
+ // Set the value of the property (stable reference)
const setPropertyValue = useCallback(
(valueOrUpdater: T | ((prevValue: T | undefined) => T)) => {
- if (!property) {
+ const prop = propertyRef.current;
+ const pathStr = pathRef.current;
+ if (!prop) {
setError(
new Error(
- `Cannot set value for property "${path}" because it was not found. Your view model instance may be undefined, or the path may be incorrect.`
+ `Cannot set value for property "${pathStr}" because it was not found. Your view model instance may be undefined, or the path may be incorrect.`
)
);
} else {
const newValue =
typeof valueOrUpdater === 'function'
- ? (valueOrUpdater as (prevValue: T | undefined) => T)(
- property.value
- )
+ ? (valueOrUpdater as (prevValue: T | undefined) => T)(prop.value)
: valueOrUpdater;
- property.value = newValue;
+ prop.value = newValue;
}
},
- [property, path]
+ []
);
return [value, setPropertyValue, error, property as unknown as P];
diff --git a/src/hooks/useViewModelInstance.ts b/src/hooks/useViewModelInstance.ts
index 7666dfc25ca1f658bf297e62b5fb0188f521fbaf..9fe0d5e870ea488a414988216f0b132ede5f9b5d 100644
--- a/src/hooks/useViewModelInstance.ts
+++ b/src/hooks/useViewModelInstance.ts
@@ -1,4 +1,5 @@
import { useMemo, useEffect, useRef } from 'react';
+import type { RefObject } from 'react';
import type { ViewModel, ViewModelInstance } from '../specs/ViewModel.nitro';
import type { RiveFile } from '../specs/RiveFile.nitro';
import type { RiveViewRef } from '../index';
@@ -27,11 +28,20 @@ export interface UseViewModelInstanceParams {
onInit?: (instance: ViewModelInstance) => void;
}
-type ViewModelSource = ViewModel | RiveFile | RiveViewRef;
+type ViewModelSource = ViewModel | RiveFile | RiveViewRef | RefObject<RiveViewRef | null>;
+
+function isRefObject(
+ source: ViewModelSource | null
+): source is RefObject<RiveViewRef | null> {
+ return source !== null && source !== undefined && 'current' in source;
+}
function isRiveViewRef(source: ViewModelSource | null): source is RiveViewRef {
return (
- source !== null && source !== undefined && 'getViewModelInstance' in source
+ source !== null &&
+ source !== undefined &&
+ !isRefObject(source) &&
+ 'getViewModelInstance' in source
);
}
@@ -43,8 +53,10 @@ function isRiveFile(source: ViewModelSource | null): source is RiveFile {
);
}
+type ResolvedViewModelSource = ViewModel | RiveFile | RiveViewRef | null;
+
function createInstance(
- source: ViewModelSource | null,
+ source: ResolvedViewModelSource,
name: string | undefined,
useNew: boolean
): { instance: ViewModelInstance | null; needsDispose: boolean } {
@@ -146,19 +158,23 @@ export function useViewModelInstance(
const required = params?.required ?? false;
const onInit = params?.onInit;
+ const resolvedSource: ResolvedViewModelSource = isRefObject(source)
+ ? source.current
+ : (source as ResolvedViewModelSource);
+
const prevInstanceRef = useRef<{
instance: ViewModelInstance | null;
needsDispose: boolean;
} | null>(null);
const result = useMemo(() => {
- const created = createInstance(source, name, useNew);
+ const created = createInstance(resolvedSource, name, useNew);
if (created.instance && onInit) {
onInit(created.instance);
}
return created;
// eslint-disable-next-line react-hooks/exhaustive-deps -- onInit excluded intentionally
- }, [source, name, useNew]);
+ }, [resolvedSource, name, useNew]);
// Dispose previous instance if it changed and needed disposal
if (
diff --git a/src/index.tsx b/src/index.tsx
index f37bd9ebaba7743baadcfc43e64a98ad1f24b410..28c874fb0d0e5977de43edb02a05e0118c1f65e5 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -43,7 +43,7 @@ export { RiveColor } from './core/RiveColor';
export { type RiveEvent, RiveEventType } from './core/Events';
export { type RiveError, RiveErrorType } from './core/Errors';
export { ArtboardByIndex, ArtboardByName } from './specs/ArtboardBy';
-export { useRive } from './hooks/useRive';
+export { useRive, type UseRiveOptions } from './hooks/useRive';
export { useRiveNumber } from './hooks/useRiveNumber';
export { useRiveString } from './hooks/useRiveString';
export { useRiveBoolean } from './hooks/useRiveBoolean';