Skip to content

Commit 6080168

Browse files
committed
feat: add useRiveList hook for list property management
1 parent a1246cb commit 6080168

4 files changed

Lines changed: 186 additions & 44 deletions

File tree

example/src/pages/DataBindingListExample.tsx

Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import {
55
ActivityIndicator,
66
TouchableOpacity,
77
} from 'react-native';
8-
import { useMemo, useState, useCallback, useRef } from 'react';
8+
import { useMemo, useState, useRef } from 'react';
99
import {
1010
Fit,
1111
RiveView,
1212
type ViewModelInstance,
1313
type RiveFile,
1414
type RiveViewRef,
1515
useRiveFile,
16+
useRiveList,
1617
} from '@rive-app/react-native';
1718
import { type Metadata } from '../helpers/metadata';
1819

@@ -65,18 +66,10 @@ function ListExample({
6566
}) {
6667
const riveRef = useRef<RiveViewRef>(null);
6768
const [isPlaying, setIsPlaying] = useState(true);
68-
const listProperty = useMemo(
69-
() => instance.listProperty('ListItemVM'),
70-
[instance]
71-
);
72-
const [listLength, setListLength] = useState(listProperty?.length ?? 0);
73-
74-
const refreshLength = useCallback(() => {
75-
setListLength(listProperty?.length ?? 0);
76-
}, [listProperty]);
69+
const { length, addInstance, removeInstanceAt, swap, getInstanceAt, error } =
70+
useRiveList('ListItemVM', instance);
7771

78-
const handleAddItem = useCallback(() => {
79-
if (!listProperty) return;
72+
const handleAddItem = () => {
8073
const buttonVM = file.viewModelByName('button VM');
8174
if (!buttonVM) {
8275
console.error('button VM view model not found');
@@ -91,54 +84,47 @@ function ListExample({
9184
if (stringProp) {
9285
stringProp.value = 'new btn';
9386
}
94-
listProperty.addInstance(newInstance);
87+
addInstance(newInstance);
9588
riveRef.current?.playIfNeeded();
96-
refreshLength();
97-
}, [listProperty, file, refreshLength]);
89+
};
9890

99-
const handleRemoveFirst = useCallback(() => {
100-
if (!listProperty || listProperty.length === 0) return;
101-
listProperty.removeInstanceAt(0);
91+
const handleRemoveFirst = () => {
92+
if (length === 0) return;
93+
removeInstanceAt(0);
10294
riveRef.current?.playIfNeeded();
103-
refreshLength();
104-
}, [listProperty, refreshLength]);
95+
};
10596

106-
const handleRemoveLast = useCallback(() => {
107-
if (!listProperty || listProperty.length === 0) return;
108-
listProperty.removeInstanceAt(listProperty.length - 1);
97+
const handleRemoveLast = () => {
98+
if (length === 0) return;
99+
removeInstanceAt(length - 1);
109100
riveRef.current?.playIfNeeded();
110-
refreshLength();
111-
}, [listProperty, refreshLength]);
101+
};
112102

113-
const handleSwapFirstTwo = useCallback(() => {
114-
if (!listProperty || listProperty.length < 2) return;
115-
listProperty.swap(0, 1);
103+
const handleSwapFirstTwo = () => {
104+
if (length < 2) return;
105+
swap(0, 1);
116106
riveRef.current?.playIfNeeded();
117-
refreshLength();
118-
}, [listProperty, refreshLength]);
119-
120-
const logListItems = useCallback(() => {
121-
if (!listProperty) return;
122-
console.log(`List has ${listProperty.length} items:`);
123-
for (let i = 0; i < listProperty.length; i++) {
124-
const item = listProperty.getInstanceAt(i);
107+
};
108+
109+
const logListItems = () => {
110+
console.log(`List has ${length} items:`);
111+
for (let i = 0; i < length; i++) {
112+
const item = getInstanceAt(i);
125113
console.log(` [${i}]: ${item?.instanceName ?? 'undefined'}`);
126114
}
127-
}, [listProperty]);
115+
};
128116

129-
const handlePlayPause = useCallback(() => {
117+
const handlePlayPause = () => {
130118
if (isPlaying) {
131119
riveRef.current?.pause();
132120
} else {
133121
riveRef.current?.play();
134122
}
135123
setIsPlaying(!isPlaying);
136-
}, [isPlaying]);
124+
};
137125

138-
if (!listProperty) {
139-
return (
140-
<Text style={styles.errorText}>ListItemVM list property not found</Text>
141-
);
126+
if (error) {
127+
return <Text style={styles.errorText}>{error.message}</Text>;
142128
}
143129

144130
return (
@@ -156,7 +142,7 @@ function ListExample({
156142
file={file}
157143
/>
158144
<View style={styles.controls}>
159-
<Text style={styles.infoText}>List length: {listLength}</Text>
145+
<Text style={styles.infoText}>List length: {length}</Text>
160146
<View style={styles.buttonRow}>
161147
<TouchableOpacity style={styles.button} onPress={handleAddItem}>
162148
<Text style={styles.buttonText}>Add Item</Text>

src/hooks/useRiveList.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { useCallback, useEffect, useState, useMemo } from 'react';
2+
import type { ViewModelInstance } from '../specs/ViewModel.nitro';
3+
import type { UseRiveListResult } from '../types';
4+
5+
/**
6+
* Hook for interacting with list ViewModel instance properties.
7+
*
8+
* @param path - The path to the list property
9+
* @param viewModelInstance - The ViewModelInstance containing the list property
10+
* @returns An object with list length, manipulation methods, and error state
11+
*/
12+
export function useRiveList(
13+
path: string,
14+
viewModelInstance?: ViewModelInstance | null
15+
): UseRiveListResult {
16+
const [error, setError] = useState<Error | null>(null);
17+
const [revision, setRevision] = useState(0);
18+
19+
useEffect(() => {
20+
setError(null);
21+
}, [path, viewModelInstance]);
22+
23+
const property = useMemo(() => {
24+
if (!viewModelInstance) return undefined;
25+
return viewModelInstance.listProperty(path);
26+
}, [viewModelInstance, path]);
27+
28+
useEffect(() => {
29+
if (viewModelInstance && !property) {
30+
setError(
31+
new Error(`List property "${path}" not found in the ViewModel instance`)
32+
);
33+
}
34+
}, [viewModelInstance, property, path]);
35+
36+
useEffect(() => {
37+
if (!property) return;
38+
39+
const removeListener = property.addListener(() => {
40+
setRevision((r) => r + 1);
41+
});
42+
43+
return () => {
44+
removeListener();
45+
property.removeListeners();
46+
property.dispose();
47+
};
48+
}, [property]);
49+
50+
const length = useMemo(() => {
51+
// revision is used to trigger re-computation when list changes
52+
53+
revision;
54+
return property?.length ?? 0;
55+
}, [property, revision]);
56+
57+
const getInstanceAt = useCallback(
58+
(index: number) => {
59+
return property?.getInstanceAt(index);
60+
},
61+
[property]
62+
);
63+
64+
const addInstance = useCallback(
65+
(instance: ViewModelInstance) => {
66+
property?.addInstance(instance);
67+
},
68+
[property]
69+
);
70+
71+
const addInstanceAt = useCallback(
72+
(instance: ViewModelInstance, index: number) => {
73+
return property?.addInstanceAt(instance, index) ?? false;
74+
},
75+
[property]
76+
);
77+
78+
const removeInstance = useCallback(
79+
(instance: ViewModelInstance) => {
80+
property?.removeInstance(instance);
81+
},
82+
[property]
83+
);
84+
85+
const removeInstanceAt = useCallback(
86+
(index: number) => {
87+
property?.removeInstanceAt(index);
88+
},
89+
[property]
90+
);
91+
92+
const swap = useCallback(
93+
(index1: number, index2: number) => {
94+
return property?.swap(index1, index2) ?? false;
95+
},
96+
[property]
97+
);
98+
99+
return {
100+
length,
101+
getInstanceAt,
102+
addInstance,
103+
addInstanceAt,
104+
removeInstance,
105+
removeInstanceAt,
106+
swap,
107+
error,
108+
};
109+
}

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export { useRiveBoolean } from './hooks/useRiveBoolean';
4848
export { useRiveEnum } from './hooks/useRiveEnum';
4949
export { useRiveColor } from './hooks/useRiveColor';
5050
export { useRiveTrigger } from './hooks/useRiveTrigger';
51+
export { useRiveList } from './hooks/useRiveList';
5152
export { useRiveFile } from './hooks/useRiveFile';
5253
export { type RiveFileInput } from './hooks/useRiveFile';
5354
export { DataBindMode };

src/types.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,49 @@ export interface UseRiveTriggerResult {
3232
export type UseViewModelInstanceTriggerParameters = {
3333
onTrigger?: () => void;
3434
};
35+
36+
export interface UseRiveListResult {
37+
/**
38+
* The number of instances in the list.
39+
*/
40+
length: number;
41+
/**
42+
* Get the instance at the given index.
43+
*/
44+
getInstanceAt: (
45+
index: number
46+
) => import('./specs/ViewModel.nitro').ViewModelInstance | undefined;
47+
/**
48+
* Add an instance to the end of the list.
49+
*/
50+
addInstance: (
51+
instance: import('./specs/ViewModel.nitro').ViewModelInstance
52+
) => void;
53+
/**
54+
* Add an instance at the given index.
55+
* @returns true if successful
56+
*/
57+
addInstanceAt: (
58+
instance: import('./specs/ViewModel.nitro').ViewModelInstance,
59+
index: number
60+
) => boolean;
61+
/**
62+
* Remove an instance from the list.
63+
*/
64+
removeInstance: (
65+
instance: import('./specs/ViewModel.nitro').ViewModelInstance
66+
) => void;
67+
/**
68+
* Remove the instance at the given index.
69+
*/
70+
removeInstanceAt: (index: number) => void;
71+
/**
72+
* Swap the instances at the given indices.
73+
* @returns true if successful
74+
*/
75+
swap: (index1: number, index2: number) => boolean;
76+
/**
77+
* The error if the property is not found.
78+
*/
79+
error: Error | null;
80+
}

0 commit comments

Comments
 (0)