From e0380f80df0c8c4eca3a1f7b269214547da9f155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 16 Dec 2025 14:34:13 +0100 Subject: [PATCH 01/13] feat: add useRiveList hook for list property management --- example/src/pages/DataBindingListExample.tsx | 74 ++++++------- src/hooks/useRiveList.ts | 108 +++++++++++++++++++ src/index.tsx | 1 + src/types.tsx | 46 ++++++++ 4 files changed, 185 insertions(+), 44 deletions(-) create mode 100644 src/hooks/useRiveList.ts diff --git a/example/src/pages/DataBindingListExample.tsx b/example/src/pages/DataBindingListExample.tsx index 4dc2b599..8fd1c955 100644 --- a/example/src/pages/DataBindingListExample.tsx +++ b/example/src/pages/DataBindingListExample.tsx @@ -5,7 +5,7 @@ import { ActivityIndicator, TouchableOpacity, } from 'react-native'; -import { useMemo, useState, useCallback, useRef } from 'react'; +import { useMemo, useState, useRef } from 'react'; import { Fit, RiveView, @@ -13,6 +13,7 @@ import { type RiveFile, type RiveViewRef, useRiveFile, + useRiveList, } from '@rive-app/react-native'; import { type Metadata } from '../helpers/metadata'; @@ -65,18 +66,10 @@ function ListExample({ }) { const riveRef = useRef(null); const [isPlaying, setIsPlaying] = useState(true); - const listProperty = useMemo( - () => instance.listProperty('ListItemVM'), - [instance] - ); - const [listLength, setListLength] = useState(listProperty?.length ?? 0); - - const refreshLength = useCallback(() => { - setListLength(listProperty?.length ?? 0); - }, [listProperty]); + const { length, addInstance, removeInstanceAt, swap, getInstanceAt, error } = + useRiveList('ListItemVM', instance); - const handleAddItem = useCallback(() => { - if (!listProperty) return; + const handleAddItem = () => { const buttonVM = file.viewModelByName('button VM'); if (!buttonVM) { console.error('button VM view model not found'); @@ -91,54 +84,47 @@ function ListExample({ if (stringProp) { stringProp.value = 'new btn'; } - listProperty.addInstance(newInstance); + addInstance(newInstance); riveRef.current?.playIfNeeded(); - refreshLength(); - }, [listProperty, file, refreshLength]); + }; - const handleRemoveFirst = useCallback(() => { - if (!listProperty || listProperty.length === 0) return; - listProperty.removeInstanceAt(0); + const handleRemoveFirst = () => { + if (length === 0) return; + removeInstanceAt(0); riveRef.current?.playIfNeeded(); - refreshLength(); - }, [listProperty, refreshLength]); + }; - const handleRemoveLast = useCallback(() => { - if (!listProperty || listProperty.length === 0) return; - listProperty.removeInstanceAt(listProperty.length - 1); + const handleRemoveLast = () => { + if (length === 0) return; + removeInstanceAt(length - 1); riveRef.current?.playIfNeeded(); - refreshLength(); - }, [listProperty, refreshLength]); + }; - const handleSwapFirstTwo = useCallback(() => { - if (!listProperty || listProperty.length < 2) return; - listProperty.swap(0, 1); + const handleSwapFirstTwo = () => { + if (length < 2) return; + swap(0, 1); riveRef.current?.playIfNeeded(); - refreshLength(); - }, [listProperty, refreshLength]); - - const logListItems = useCallback(() => { - if (!listProperty) return; - console.log(`List has ${listProperty.length} items:`); - for (let i = 0; i < listProperty.length; i++) { - const item = listProperty.getInstanceAt(i); + }; + + const logListItems = () => { + console.log(`List has ${length} items:`); + for (let i = 0; i < length; i++) { + const item = getInstanceAt(i); console.log(` [${i}]: ${item?.instanceName ?? 'undefined'}`); } - }, [listProperty]); + }; - const handlePlayPause = useCallback(() => { + const handlePlayPause = () => { if (isPlaying) { riveRef.current?.pause(); } else { riveRef.current?.play(); } setIsPlaying(!isPlaying); - }, [isPlaying]); + }; - if (!listProperty) { - return ( - ListItemVM list property not found - ); + if (error) { + return {error.message}; } return ( @@ -156,7 +142,7 @@ function ListExample({ file={file} /> - List length: {listLength} + List length: {length} Add Item diff --git a/src/hooks/useRiveList.ts b/src/hooks/useRiveList.ts new file mode 100644 index 00000000..71d09785 --- /dev/null +++ b/src/hooks/useRiveList.ts @@ -0,0 +1,108 @@ +import { useCallback, useEffect, useState, useMemo } from 'react'; +import type { ViewModelInstance } from '../specs/ViewModel.nitro'; +import type { UseRiveListResult } from '../types'; + +/** + * Hook for interacting with list ViewModel instance properties. + * + * @param path - The path to the list property + * @param viewModelInstance - The ViewModelInstance containing the list property + * @returns An object with list length, manipulation methods, and error state + */ +export function useRiveList( + path: string, + viewModelInstance?: ViewModelInstance | null +): UseRiveListResult { + const [error, setError] = useState(null); + const [revision, setRevision] = useState(0); + + useEffect(() => { + setError(null); + }, [path, viewModelInstance]); + + const property = useMemo(() => { + if (!viewModelInstance) return undefined; + return viewModelInstance.listProperty(path); + }, [viewModelInstance, path]); + + useEffect(() => { + if (viewModelInstance && !property) { + setError( + new Error(`List property "${path}" not found in the ViewModel instance`) + ); + } + }, [viewModelInstance, property, path]); + + useEffect(() => { + if (!property) return; + + const removeListener = property.addListener(() => { + setRevision((r) => r + 1); + }); + + return () => { + removeListener(); + property.removeListeners(); + property.dispose(); + }; + }, [property]); + + const length = useMemo(() => { + // revision is used to trigger re-computation when list changes + revision; + return property?.length ?? 0; + }, [property, revision]); + + const getInstanceAt = useCallback( + (index: number) => { + return property?.getInstanceAt(index); + }, + [property] + ); + + const addInstance = useCallback( + (instance: ViewModelInstance) => { + property?.addInstance(instance); + }, + [property] + ); + + const addInstanceAt = useCallback( + (instance: ViewModelInstance, index: number) => { + return property?.addInstanceAt(instance, index) ?? false; + }, + [property] + ); + + const removeInstance = useCallback( + (instance: ViewModelInstance) => { + property?.removeInstance(instance); + }, + [property] + ); + + const removeInstanceAt = useCallback( + (index: number) => { + property?.removeInstanceAt(index); + }, + [property] + ); + + const swap = useCallback( + (index1: number, index2: number) => { + return property?.swap(index1, index2) ?? false; + }, + [property] + ); + + return { + length, + getInstanceAt, + addInstance, + addInstanceAt, + removeInstance, + removeInstanceAt, + swap, + error, + }; +} diff --git a/src/index.tsx b/src/index.tsx index 5896add0..21bc9c26 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -48,6 +48,7 @@ export { useRiveBoolean } from './hooks/useRiveBoolean'; export { useRiveEnum } from './hooks/useRiveEnum'; export { useRiveColor } from './hooks/useRiveColor'; export { useRiveTrigger } from './hooks/useRiveTrigger'; +export { useRiveList } from './hooks/useRiveList'; export { useRiveFile } from './hooks/useRiveFile'; export { type RiveFileInput } from './hooks/useRiveFile'; export { DataBindMode }; diff --git a/src/types.tsx b/src/types.tsx index 2fd165bf..a7b5713f 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -32,3 +32,49 @@ export interface UseRiveTriggerResult { export type UseViewModelInstanceTriggerParameters = { onTrigger?: () => void; }; + +export interface UseRiveListResult { + /** + * The number of instances in the list. + */ + length: number; + /** + * Get the instance at the given index. + */ + getInstanceAt: ( + index: number + ) => import('./specs/ViewModel.nitro').ViewModelInstance | undefined; + /** + * Add an instance to the end of the list. + */ + addInstance: ( + instance: import('./specs/ViewModel.nitro').ViewModelInstance + ) => void; + /** + * Add an instance at the given index. + * @returns true if successful + */ + addInstanceAt: ( + instance: import('./specs/ViewModel.nitro').ViewModelInstance, + index: number + ) => boolean; + /** + * Remove an instance from the list. + */ + removeInstance: ( + instance: import('./specs/ViewModel.nitro').ViewModelInstance + ) => void; + /** + * Remove the instance at the given index. + */ + removeInstanceAt: (index: number) => void; + /** + * Swap the instances at the given indices. + * @returns true if successful + */ + swap: (index1: number, index2: number) => boolean; + /** + * The error if the property is not found. + */ + error: Error | null; +} From 2a884130001efe5047f2dcb998742595870b40e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Tue, 16 Dec 2025 14:50:21 +0100 Subject: [PATCH 02/13] feat: add MenuList example adapted from rive-react codesandbox --- example/assets/lists_demo.rev | Bin 0 -> 347018 bytes example/assets/lists_demo.riv | Bin 0 -> 331999 bytes example/src/pages/MenuListExample.tsx | 326 ++++++++++++++++++++++++++ example/src/pages/index.ts | 1 + 4 files changed, 327 insertions(+) create mode 100644 example/assets/lists_demo.rev create mode 100644 example/assets/lists_demo.riv create mode 100644 example/src/pages/MenuListExample.tsx diff --git a/example/assets/lists_demo.rev b/example/assets/lists_demo.rev new file mode 100644 index 0000000000000000000000000000000000000000..c56bc3caaebafc4bcf45c3aff3d13655367ed685 GIT binary patch literal 347018 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