Skip to content

Commit 26223f2

Browse files
authored
fix: read initial value in useRiveProperty hooks (#97)
## Summary - `useRiveProperty` (and all hooks using it like `useRiveEnum`, `useRiveNumber`, etc.) returned `undefined` on initial render even when the property had a value - Fixed by retrieving the property first, then initializing state with its current value ## Test plan - [x] Added unit tests for initial value, path changes, and instance changes - [x] All tests pass
1 parent ee32c69 commit 26223f2

2 files changed

Lines changed: 164 additions & 9 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { renderHook, act } from '@testing-library/react-native';
2+
import { useRiveProperty } from '../useRiveProperty';
3+
import type { ViewModelInstance } from '../../specs/ViewModel.nitro';
4+
5+
describe('useRiveProperty', () => {
6+
const createMockProperty = (initialValue: string) => {
7+
let currentValue = initialValue;
8+
let listener: ((value: string) => void) | null = null;
9+
10+
return {
11+
get value() {
12+
return currentValue;
13+
},
14+
set value(newValue: string) {
15+
currentValue = newValue;
16+
listener?.(newValue);
17+
},
18+
addListener: jest.fn((callback: (value: string) => void) => {
19+
listener = callback;
20+
return () => {
21+
listener = null;
22+
};
23+
}),
24+
dispose: jest.fn(),
25+
};
26+
};
27+
28+
const createMockViewModelInstance = (
29+
propertyMap: Record<string, ReturnType<typeof createMockProperty>>
30+
) => {
31+
return {
32+
enumProperty: jest.fn((path: string) => propertyMap[path]),
33+
numberProperty: jest.fn((path: string) => propertyMap[path]),
34+
stringProperty: jest.fn((path: string) => propertyMap[path]),
35+
booleanProperty: jest.fn((path: string) => propertyMap[path]),
36+
} as unknown as ViewModelInstance;
37+
};
38+
39+
it('should return initial value from property on first render', () => {
40+
const mockProperty = createMockProperty('Tea');
41+
const mockInstance = createMockViewModelInstance({
42+
'favDrink/type': mockProperty,
43+
});
44+
45+
const { result } = renderHook(() =>
46+
useRiveProperty<any, string>(mockInstance, 'favDrink/type', {
47+
getProperty: (vmi, path) => (vmi as any).enumProperty(path),
48+
})
49+
);
50+
51+
const [value] = result.current;
52+
expect(value).toBe('Tea');
53+
});
54+
55+
it('should update value when property changes', () => {
56+
const mockProperty = createMockProperty('Tea');
57+
const mockInstance = createMockViewModelInstance({
58+
'favDrink/type': mockProperty,
59+
});
60+
61+
const { result } = renderHook(() =>
62+
useRiveProperty<any, string>(mockInstance, 'favDrink/type', {
63+
getProperty: (vmi, path) => (vmi as any).enumProperty(path),
64+
})
65+
);
66+
67+
act(() => {
68+
mockProperty.value = 'Coffee';
69+
});
70+
71+
const [value] = result.current;
72+
expect(value).toBe('Coffee');
73+
});
74+
75+
it('should return undefined when viewModelInstance is null', () => {
76+
const { result } = renderHook(() =>
77+
useRiveProperty<any, string>(null, 'favDrink/type', {
78+
getProperty: (vmi, path) => (vmi as any).enumProperty(path),
79+
})
80+
);
81+
82+
const [value] = result.current;
83+
expect(value).toBeUndefined();
84+
});
85+
86+
it('should return error when property is not found', () => {
87+
const mockInstance = createMockViewModelInstance({});
88+
89+
const { result } = renderHook(() =>
90+
useRiveProperty<any, string>(mockInstance, 'nonexistent/path', {
91+
getProperty: (vmi, path) => (vmi as any).enumProperty(path),
92+
})
93+
);
94+
95+
const [, , error] = result.current;
96+
expect(error).toBeInstanceOf(Error);
97+
expect(error?.message).toContain('nonexistent/path');
98+
});
99+
100+
it('should update value when path changes', () => {
101+
const teaProperty = createMockProperty('Tea');
102+
const coffeeProperty = createMockProperty('Coffee');
103+
const mockInstance = createMockViewModelInstance({
104+
'drinks/tea': teaProperty,
105+
'drinks/coffee': coffeeProperty,
106+
});
107+
108+
const { result, rerender } = renderHook(
109+
(props: { path: string }) =>
110+
useRiveProperty<any, string>(mockInstance, props.path, {
111+
getProperty: (vmi, p) => (vmi as any).enumProperty(p),
112+
}),
113+
{ initialProps: { path: 'drinks/tea' } }
114+
);
115+
116+
expect(result.current[0]).toBe('Tea');
117+
118+
rerender({ path: 'drinks/coffee' });
119+
120+
expect(result.current[0]).toBe('Coffee');
121+
});
122+
123+
it('should update value when viewModelInstance changes', () => {
124+
const instance1Property = createMockProperty('Instance1Value');
125+
const instance2Property = createMockProperty('Instance2Value');
126+
const mockInstance1 = createMockViewModelInstance({
127+
'prop/path': instance1Property,
128+
});
129+
const mockInstance2 = createMockViewModelInstance({
130+
'prop/path': instance2Property,
131+
});
132+
133+
const { result, rerender } = renderHook(
134+
(props: { instance: ViewModelInstance }) =>
135+
useRiveProperty<any, string>(props.instance, 'prop/path', {
136+
getProperty: (vmi, p) => (vmi as any).enumProperty(p),
137+
}),
138+
{ initialProps: { instance: mockInstance1 } }
139+
);
140+
141+
expect(result.current[0]).toBe('Instance1Value');
142+
143+
rerender({ instance: mockInstance2 });
144+
145+
expect(result.current[0]).toBe('Instance2Value');
146+
});
147+
});

src/hooks/useRiveProperty.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,7 @@ export function useRiveProperty<P extends ViewModelProperty, T>(
3434
Error | null,
3535
P | undefined,
3636
] {
37-
const [value, setValue] = useState<T | undefined>(undefined);
38-
const [error, setError] = useState<Error | null>(null);
39-
40-
// Clear error when path or instance changes
41-
useEffect(() => {
42-
setError(null);
43-
}, [path, viewModelInstance]);
44-
45-
// Get the property
37+
// Get the property first so we can read its initial value
4638
const property = useMemo(() => {
4739
if (!viewModelInstance) return;
4840
return options.getProperty(
@@ -51,6 +43,22 @@ export function useRiveProperty<P extends ViewModelProperty, T>(
5143
) as unknown as ObservableViewModelProperty<T>;
5244
}, [options, viewModelInstance, path]);
5345

46+
// Initialize state with property's current value (if available)
47+
const [value, setValue] = useState<T | undefined>(() => property?.value);
48+
const [error, setError] = useState<Error | null>(null);
49+
50+
// Sync value when property reference changes (path or instance changed)
51+
useEffect(() => {
52+
if (property) {
53+
setValue(property.value);
54+
}
55+
}, [property]);
56+
57+
// Clear error when path or instance changes
58+
useEffect(() => {
59+
setError(null);
60+
}, [path, viewModelInstance]);
61+
5462
// Set error if property is not found
5563
useEffect(() => {
5664
if (viewModelInstance && !property) {

0 commit comments

Comments
 (0)