Skip to content

Commit b249e94

Browse files
committed
fix: useRiveProperty hooks start undefined, value delivered via listener
Initial value is no longer read synchronously from property.value. Instead the hook always starts as undefined and receives the current value through the listener's first emission. This unifies the startup shape across legacy (sync emit) and experimental (async valueStream) backends, so consumers always handle the loading state. The setter callback now uses React state for updater functions instead of reading property.value synchronously.
1 parent d5b1687 commit b249e94

2 files changed

Lines changed: 17 additions & 16 deletions

File tree

src/hooks/__tests__/useRiveProperty.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ describe('useRiveProperty', () => {
1717
},
1818
addListener: jest.fn((callback: (value: string) => void) => {
1919
listener = callback;
20+
// Emit the current value immediately on subscribe, matching native behaviour:
21+
// iOS legacy emits synchronously; experimental backend emits via valueStream.
22+
callback(currentValue);
2023
return () => {
2124
listener = null;
2225
};
@@ -36,7 +39,9 @@ describe('useRiveProperty', () => {
3639
} as unknown as ViewModelInstance;
3740
};
3841

39-
it('should return initial value from property on first render', () => {
42+
it('should return initial value delivered via listener (not from a sync read)', () => {
43+
// Hooks always start undefined; the listener emits the current value immediately
44+
// on subscribe (synchronously for legacy, via stream for experimental).
4045
const mockProperty = createMockProperty('Tea');
4146
const mockInstance = createMockViewModelInstance({
4247
'favDrink/type': mockProperty,
@@ -48,6 +53,8 @@ describe('useRiveProperty', () => {
4853
})
4954
);
5055

56+
// The mock's addListener emits 'Tea' synchronously — React batches it with the
57+
// effect, so the value is available after renderHook (which wraps in act()).
5158
const [value] = result.current;
5259
expect(value).toBe('Tea');
5360
});

src/hooks/useRiveProperty.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export function useRiveProperty<P extends ViewModelProperty, T>(
3434
Error | null,
3535
P | undefined,
3636
] {
37-
// Get the property first so we can read its initial value
3837
const property = useMemo(() => {
3938
if (!viewModelInstance) return;
4039
return options.getProperty(
@@ -43,17 +42,12 @@ export function useRiveProperty<P extends ViewModelProperty, T>(
4342
) as unknown as ObservableViewModelProperty<T>;
4443
}, [options, viewModelInstance, path]);
4544

46-
// Initialize state with property's current value (if available)
47-
const [value, setValue] = useState<T | undefined>(() => property?.value);
45+
// Always start undefined — the listener delivers the current value as its first emission.
46+
// (iOS experimental: via valueStream; iOS/Android legacy: emitted synchronously on subscribe)
47+
// This ensures consumers handle the loading state correctly on all backends.
48+
const [value, setValue] = useState<T | undefined>(undefined);
4849
const [error, setError] = useState<Error | null>(null);
4950

50-
// Sync value when property reference changes (path or instance changed)
51-
useEffect(() => {
52-
if (property) {
53-
setValue(property.value);
54-
}
55-
}, [property]);
56-
5751
// Clear error when path or instance changes
5852
useEffect(() => {
5953
setError(null);
@@ -86,22 +80,22 @@ export function useRiveProperty<P extends ViewModelProperty, T>(
8680
};
8781
}, [options, property]);
8882

89-
// Set the value of the property (no-op if property isn't available yet)
83+
// Set the value of the property (no-op if property isn't available yet).
84+
// Uses tracked `value` from state for updater functions — avoids a synchronous
85+
// property.value read and is consistent with how React state works.
9086
const setPropertyValue = useCallback(
9187
(valueOrUpdater: T | ((prevValue: T | undefined) => T)) => {
9288
if (!property) {
9389
return;
9490
} else {
9591
const newValue =
9692
typeof valueOrUpdater === 'function'
97-
? (valueOrUpdater as (prevValue: T | undefined) => T)(
98-
property.value
99-
)
93+
? (valueOrUpdater as (prevValue: T | undefined) => T)(value)
10094
: valueOrUpdater;
10195
property.value = newValue;
10296
}
10397
},
104-
[property]
98+
[property, value]
10599
);
106100

107101
return [value, setPropertyValue, error, property as unknown as P];

0 commit comments

Comments
 (0)