Skip to content

Commit d03b16a

Browse files
committed
feat: suspense POC
1 parent bb01f08 commit d03b16a

File tree

11 files changed

+544
-110
lines changed

11 files changed

+544
-110
lines changed

example/src/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import EventsExample from './pages/RiveEventsExample';
1414
import StateMachineInputsExample from './pages/RiveStateMachineInputsExample';
1515
import TextRunExample from './pages/RiveTextRunExample';
1616
import OutOfBandAssets from './pages/OutOfBandAssets';
17+
import RiveSuspenseExample from './pages/RiveSuspenseExample';
1718

1819
const Examples = [
1920
{
@@ -46,6 +47,11 @@ const Examples = [
4647
screenId: 'OutOfBandAssets',
4748
component: OutOfBandAssets,
4849
},
50+
{
51+
title: 'Rive Suspense Example',
52+
screenId: 'RiveSuspense',
53+
component: RiveSuspenseExample,
54+
},
4955
{ title: 'Template Page', screenId: 'Template', component: TemplatePage },
5056
] as const;
5157

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import {
2+
Text,
3+
View,
4+
StyleSheet,
5+
ActivityIndicator,
6+
ScrollView,
7+
} from 'react-native';
8+
import { Fit, RiveView, RiveSuspense } from 'react-native-rive';
9+
import { Suspense, Component, type ReactNode } from 'react';
10+
11+
class ErrorBoundary extends Component<
12+
{ children: ReactNode; fallback: ReactNode },
13+
{ hasError: boolean; error?: Error }
14+
> {
15+
constructor(props: { children: ReactNode; fallback: ReactNode }) {
16+
super(props);
17+
this.state = { hasError: false };
18+
}
19+
20+
static getDerivedStateFromError(error: Error) {
21+
return { hasError: true, error };
22+
}
23+
24+
render() {
25+
if (this.state.hasError) {
26+
return this.props.fallback;
27+
}
28+
return this.props.children;
29+
}
30+
}
31+
32+
const RiveCard = ({ title, input }: { title: string; input: any }) => {
33+
const riveFile = RiveSuspense.useRiveFile(input);
34+
35+
return (
36+
<View style={styles.card}>
37+
<Text style={styles.cardTitle}>{title}</Text>
38+
<RiveView
39+
style={styles.riveView}
40+
autoBind={false}
41+
autoPlay={true}
42+
fit={Fit.Contain}
43+
file={riveFile}
44+
/>
45+
</View>
46+
);
47+
};
48+
49+
const SuspenseContent = () => {
50+
return (
51+
<ScrollView style={styles.scrollView}>
52+
<Text style={styles.description}>
53+
This example demonstrates the Suspense API. All three cards below use
54+
the same Rive file, which is cached and shared efficiently.
55+
</Text>
56+
57+
<RiveCard
58+
title="Card 1"
59+
input={require('../../assets/rive/rating.riv')}
60+
/>
61+
<RiveCard
62+
title="Card 2"
63+
input={require('../../assets/rive/rating.riv')}
64+
/>
65+
<RiveCard
66+
title="Card 3"
67+
input={require('../../assets/rive/rating.riv')}
68+
/>
69+
70+
<Text style={styles.note}>
71+
Note: All three cards share the same cached RiveFile instance, loaded
72+
only once!
73+
</Text>
74+
</ScrollView>
75+
);
76+
};
77+
78+
export default function RiveSuspenseExample() {
79+
return (
80+
<View style={styles.container}>
81+
<Text style={styles.title}>RiveSuspense Example</Text>
82+
83+
<RiveSuspense.Provider>
84+
<ErrorBoundary
85+
fallback={
86+
<View style={styles.errorContainer}>
87+
<Text style={styles.errorText}>
88+
Failed to load Rive animation
89+
</Text>
90+
</View>
91+
}
92+
>
93+
<Suspense
94+
fallback={
95+
<View style={styles.loadingContainer}>
96+
<ActivityIndicator size="large" color="#007AFF" />
97+
<Text style={styles.loadingText}>Loading animations...</Text>
98+
</View>
99+
}
100+
>
101+
<SuspenseContent />
102+
</Suspense>
103+
</ErrorBoundary>
104+
</RiveSuspense.Provider>
105+
</View>
106+
);
107+
}
108+
109+
const styles = StyleSheet.create({
110+
container: {
111+
flex: 1,
112+
backgroundColor: '#fff',
113+
},
114+
title: {
115+
fontSize: 24,
116+
fontWeight: 'bold',
117+
textAlign: 'center',
118+
marginTop: 20,
119+
marginBottom: 10,
120+
color: '#333',
121+
},
122+
description: {
123+
fontSize: 14,
124+
textAlign: 'center',
125+
marginHorizontal: 20,
126+
marginBottom: 20,
127+
color: '#666',
128+
lineHeight: 20,
129+
},
130+
scrollView: {
131+
flex: 1,
132+
},
133+
card: {
134+
margin: 20,
135+
padding: 15,
136+
backgroundColor: '#f5f5f5',
137+
borderRadius: 10,
138+
shadowColor: '#000',
139+
shadowOffset: { width: 0, height: 2 },
140+
shadowOpacity: 0.1,
141+
shadowRadius: 4,
142+
elevation: 3,
143+
},
144+
cardTitle: {
145+
fontSize: 18,
146+
fontWeight: '600',
147+
marginBottom: 10,
148+
color: '#333',
149+
},
150+
riveView: {
151+
height: 200,
152+
backgroundColor: '#fff',
153+
borderRadius: 8,
154+
},
155+
loadingContainer: {
156+
flex: 1,
157+
justifyContent: 'center',
158+
alignItems: 'center',
159+
},
160+
loadingText: {
161+
marginTop: 10,
162+
fontSize: 16,
163+
color: '#007AFF',
164+
},
165+
errorContainer: {
166+
flex: 1,
167+
justifyContent: 'center',
168+
alignItems: 'center',
169+
},
170+
errorText: {
171+
color: 'red',
172+
fontSize: 16,
173+
textAlign: 'center',
174+
padding: 20,
175+
},
176+
note: {
177+
fontSize: 12,
178+
fontStyle: 'italic',
179+
textAlign: 'center',
180+
marginHorizontal: 20,
181+
marginBottom: 40,
182+
color: '#007AFF',
183+
},
184+
});

src/core/RiveFile.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import type {
44
RiveFileFactory as RiveFileFactoryInternal,
55
} from '../specs/RiveFile.nitro';
66

7-
// This import path isn't handled by @types/react-native
8-
// @ts-ignore
9-
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
7+
import { Image } from 'react-native';
108
import type { ResolvedReferencedAssets } from '../hooks/useRiveFile';
119

1210
const RiveFileInternal =
@@ -119,7 +117,9 @@ export namespace RiveFileFactory {
119117
const assetID = typeof source === 'number' ? source : null;
120118
const sourceURI = typeof source === 'object' ? source.uri : null;
121119

122-
const assetURI = assetID ? resolveAssetSource(assetID)?.uri : sourceURI;
120+
const assetURI = assetID
121+
? Image.resolveAssetSource(assetID)?.uri
122+
: sourceURI;
123123

124124
if (!assetURI) {
125125
throw new Error(
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useRef, useEffect } from 'react';
2+
import type { RiveFile } from '../specs/RiveFile.nitro';
3+
import type { ResolvedReferencedAssets } from '../utils/riveFileLoading';
4+
5+
export function useReferencedAssetsUpdate(
6+
riveFile: RiveFile | null,
7+
referencedAssets: ResolvedReferencedAssets | undefined
8+
) {
9+
const initialReferencedAssets = useRef(referencedAssets);
10+
11+
useEffect(() => {
12+
if (initialReferencedAssets.current !== referencedAssets) {
13+
if (riveFile && referencedAssets) {
14+
riveFile.updateReferencedAssets({ data: referencedAssets });
15+
initialReferencedAssets.current = referencedAssets;
16+
}
17+
}
18+
}, [referencedAssets, riveFile]);
19+
}

0 commit comments

Comments
 (0)