Skip to content

Commit 059e5f4

Browse files
authored
fix!: hooks start undefined, useViewModelInstance returns {instance, error} (#184)
Breaking changes to improve state transparency and prepare for the async experimental runtime. **`useRiveFile`** error is now `Error` instead of `string`, and `riveFile` is `undefined` while loading (was `null`). `isLoading` is kept for convenience. Migration: `{error}` in JSX → `{error.message}`, and if checking `riveFile` type, `null` now means error not loading. **`useViewModelInstance`** returns `{ instance, error }` discriminated union instead of `ViewModelInstance | null`. Now accepts `undefined` source so you can pass `useRiveFile().riveFile` directly: - `{ instance: undefined, error: null }` — loading (source not ready) - `{ instance: ViewModelInstance, error: null }` — success - `{ instance: null, error: null }` — resolved, no ViewModel - `{ instance: null, error: Error }` — lookup failed `required: true` throws on `null` but not `undefined` (loading). Migration: `const vmi = useViewModelInstance(file)` → `const { instance, error } = useViewModelInstance(file)`. **`useRiveNumber/String/Boolean/Color/Enum` hooks** start as `undefined` instead of reading `property.value` synchronously. The real value arrives via the listener's first emission.
1 parent 1d00468 commit 059e5f4

22 files changed

Lines changed: 231 additions & 105 deletions

example/__tests__/hooks.harness.tsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ type UseRiveNumberContext = {
2626
value: number | undefined;
2727
error: Error | null;
2828
setValue: ((v: number) => void) | null;
29+
renderValues: (number | undefined)[];
2930
};
3031

3132
function createUseRiveNumberContext(): UseRiveNumberContext {
32-
return { value: undefined, error: null, setValue: null };
33+
return { value: undefined, error: null, setValue: null, renderValues: [] };
3334
}
3435

3536
type UseViewModelInstanceContext = {
36-
instance: ViewModelInstance | null;
37+
instance: ViewModelInstance | null | undefined;
3738
age: number | undefined;
3839
};
3940

@@ -50,6 +51,8 @@ function UseRiveNumberTestComponent({
5051
}) {
5152
const { value, setValue, error } = useRiveNumber('health', instance);
5253

54+
context.renderValues.push(value);
55+
5356
useEffect(() => {
5457
context.value = value;
5558
context.error = error;
@@ -70,7 +73,7 @@ function UseViewModelInstanceTestComponent({
7073
file: RiveFile;
7174
context: UseViewModelInstanceContext;
7275
}) {
73-
const instance = useViewModelInstance(file);
76+
const { instance } = useViewModelInstance(file);
7477

7578
const age = useMemo(() => {
7679
if (!instance) return undefined;
@@ -96,6 +99,34 @@ function expectDefined<T>(value: T): asserts value is NonNullable<T> {
9699
}
97100

98101
describe('useRiveNumber Hook', () => {
102+
it('starts undefined then receives value via listener', async () => {
103+
const file = await RiveFileFactory.fromSource(QUICK_START, undefined);
104+
const vm = file.defaultArtboardViewModel();
105+
expectDefined(vm);
106+
const instance = vm.createDefaultInstance();
107+
expectDefined(instance);
108+
109+
const context = createUseRiveNumberContext();
110+
111+
await render(
112+
<UseRiveNumberTestComponent instance={instance} context={context} />
113+
);
114+
115+
// First render must produce undefined — not a synchronous read from property.value
116+
expect(context.renderValues[0]).toBeUndefined();
117+
118+
// After listener fires, value should be a number
119+
await waitFor(
120+
() => {
121+
expect(context.error).toBeNull();
122+
expect(typeof context.value).toBe('number');
123+
},
124+
{ timeout: 5000 }
125+
);
126+
127+
cleanup();
128+
});
129+
99130
it('returns value from number property', async () => {
100131
const file = await RiveFileFactory.fromSource(QUICK_START, undefined);
101132
const vm = file.defaultArtboardViewModel();

example/src/demos/DataBindingArtboardsExample.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default function DataBindingArtboardsExample() {
6262
return (
6363
<View style={styles.container}>
6464
<Text style={styles.errorText}>
65-
{error || 'Failed to load Rive files'}
65+
{error?.message || 'Failed to load Rive files'}
6666
</Text>
6767
</View>
6868
);
@@ -78,7 +78,7 @@ function ArtboardSwapper({
7878
mainFile: RiveFile;
7979
assetsFile: RiveFile;
8080
}) {
81-
const instance = useViewModelInstance(mainFile);
81+
const { instance, error } = useViewModelInstance(mainFile);
8282
const [currentArtboard, setCurrentArtboard] = useState<string>('Dragon');
8383
const initializedRef = useRef(false);
8484

@@ -98,6 +98,11 @@ function ArtboardSwapper({
9898
}
9999
}, [instance, assetsFile]);
100100

101+
if (error) {
102+
console.error(error.message);
103+
return <Text style={{ color: 'red' }}>{error.message}</Text>;
104+
}
105+
101106
// Map display names to actual artboard names
102107
const artboardOptions = [
103108
{ label: 'Dragon', artboard: 'Character 1', fromAssets: true },

example/src/demos/QuickStart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default function QuickStart() {
2323
require('../../assets/rive/quick_start.riv')
2424
);
2525
const { riveViewRef, setHybridRef } = useRive();
26-
const viewModelInstance = useViewModelInstance(riveFile, {
26+
const { instance: viewModelInstance } = useViewModelInstance(riveFile, {
2727
onInit: (vmi) => vmi.numberProperty('health')!.set(9),
2828
});
2929

example/src/exercisers/FontFallbackExample.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ function MountedView({ text }: { text: string }) {
258258
// https://rive.app/marketplace/26480-49641-simple-test-text-property/
259259
require('../../assets/rive/font_fallback.riv')
260260
);
261-
const instance = useViewModelInstance(riveFile ?? null);
261+
const { instance } = useViewModelInstance(riveFile);
262262

263263
const { setValue: setRiveText, error: textError } = useRiveString(
264264
TEXT_PROPERTY,
@@ -285,7 +285,9 @@ function MountedView({ text }: { text: string }) {
285285
if (error || !riveFile) {
286286
return (
287287
<View style={styles.riveContainer}>
288-
<Text style={styles.errorText}>{error || 'Failed to load file'}</Text>
288+
<Text style={styles.errorText}>
289+
{error?.message || 'Failed to load file'}
290+
</Text>
289291
</View>
290292
);
291293
}

example/src/exercisers/MenuListExample.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,21 @@ export default function MenuListExample() {
3232
) : riveFile ? (
3333
<MenuList file={riveFile} />
3434
) : (
35-
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
35+
<Text style={styles.errorText}>
36+
{error?.message || 'Unexpected error'}
37+
</Text>
3638
)}
3739
</View>
3840
);
3941
}
4042

4143
function MenuList({ file }: { file: RiveFile }) {
42-
const instance = useViewModelInstance(file, { required: true });
44+
const { instance, error } = useViewModelInstance(file);
45+
46+
if (error) {
47+
console.error(error.message);
48+
return <Text style={{ color: 'red' }}>{error.message}</Text>;
49+
}
4350

4451
if (!instance) {
4552
return <ActivityIndicator size="large" color="#007AFF" />;

example/src/exercisers/NestedViewModelExample.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,21 @@ export default function NestedViewModelExample() {
3131
) : riveFile ? (
3232
<WithViewModelSetup file={riveFile} />
3333
) : (
34-
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
34+
<Text style={styles.errorText}>
35+
{error?.message || 'Unexpected error'}
36+
</Text>
3537
)}
3638
</View>
3739
);
3840
}
3941

4042
function WithViewModelSetup({ file }: { file: RiveFile }) {
41-
const instance = useViewModelInstance(file);
43+
const { instance, error } = useViewModelInstance(file);
44+
45+
if (error) {
46+
console.error(error.message);
47+
return <Text style={{ color: 'red' }}>{error.message}</Text>;
48+
}
4249

4350
if (!instance) {
4451
return <ActivityIndicator size="large" color="#0000ff" />;

example/src/exercisers/OutOfBandAssets.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ export default function OutOfBandAssetsExample() {
4949

5050
if (isLoading) {
5151
return <ActivityIndicator />;
52-
} else if (error != null) {
52+
} else if (error) {
5353
return (
5454
<View style={styles.safeAreaViewContainer}>
55-
<Text>Error loading Rive file</Text>
55+
<Text>Error loading Rive file: {error.message}</Text>
5656
</View>
5757
);
5858
}

example/src/exercisers/OutOfBandAssetsWithSuspense.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ function RiveContent({ imageUrl }: { imageUrl: ImageURLS }) {
102102

103103
if (isLoading) {
104104
return <ActivityIndicator />;
105-
} else if (error != null) {
105+
} else if (error) {
106106
return (
107107
<View style={styles.safeAreaViewContainer}>
108-
<Text>Error loading Rive file: {error}</Text>
108+
<Text>Error loading Rive file: {error.message}</Text>
109109
</View>
110110
);
111111
}

example/src/exercisers/ResponsiveLayouts.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default function ResponsiveLayoutsExample() {
4646
{isLoading ? (
4747
<ActivityIndicator size="large" color="#0000ff" />
4848
) : error ? (
49-
<Text style={styles.errorText}>{error}</Text>
49+
<Text style={styles.errorText}>{error.message}</Text>
5050
) : riveFile ? (
5151
<RiveView
5252
hybridRef={{ f: (ref) => (riveRef.current = ref) }}

example/src/exercisers/RiveDataBindingExample.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,22 @@ export default function WithRiveFile() {
2727
) : riveFile ? (
2828
<WithViewModelSetup file={riveFile} />
2929
) : (
30-
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
30+
<Text style={styles.errorText}>
31+
{error?.message || 'Unexpected error'}
32+
</Text>
3133
)}
3234
</View>
3335
</View>
3436
);
3537
}
3638

3739
function WithViewModelSetup({ file }: { file: RiveFile }) {
38-
const instance = useViewModelInstance(file);
40+
const { instance, error } = useViewModelInstance(file);
41+
42+
if (error) {
43+
console.error(error.message);
44+
return <Text style={{ color: 'red' }}>{error.message}</Text>;
45+
}
3946

4047
if (!instance) {
4148
return <ActivityIndicator size="large" color="#0000ff" />;

0 commit comments

Comments
 (0)