Building a Photo Sharing App with Expo & React Native with Imgwire
This tutorial shows how to pick photos from an Expo app, upload them to Imgwire, store the resulting image IDs, and render optimized feed images in React Native.
Install packages
yarn add @imgwire/react-native expo-image-picker
Create an Imgwire Client Key for the mobile app. For production apps, prefer signed uploads and have your mobile app request upload tokens from your backend.
Add the provider
Wrap your app with ImgwireProvider:
import { ImgwireProvider } from '@imgwire/react-native';
import { FeedScreen } from './FeedScreen';
export default function App() {
return (
<ImgwireProvider
config={{
apiKey: process.env.EXPO_PUBLIC_IMGWIRE_CLIENT_KEY!,
getUploadToken: async () => {
const response = await fetch(
'https://api.example.com/imgwire/upload-token',
{
method: 'POST',
}
);
if (!response.ok) {
throw new Error('Unable to create Imgwire upload token');
}
const { uploadToken } = await response.json();
return uploadToken;
},
}}
>
<FeedScreen />
</ImgwireProvider>
);
}
Use your own API URL for getUploadToken. Do not put a Server API Key in the mobile app bundle.
Pick and upload a photo
Use expo-image-picker to choose a device image, then upload the returned file URI with useUpload.
import { useState } from 'react';
import { Button, FlatList, Text, View } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { Image as ImgwireImage, useUpload } from '@imgwire/react-native';
type SharedPhoto = {
id: string;
caption: string;
};
export function FeedScreen() {
const [photos, setPhotos] = useState<SharedPhoto[]>([]);
const [upload, progress] = useUpload();
async function addPhoto() {
const result = await ImagePicker.launchImageLibraryAsync({
quality: 0.9,
});
if (result.canceled) return;
const asset = result.assets[0];
const image = await upload({
uri: asset.uri,
name: asset.fileName ?? 'shared-photo.jpg',
type: asset.mimeType ?? 'image/jpeg',
});
setPhotos((current) => [
{
id: image.id,
caption: 'Shared from Expo',
},
...current,
]);
}
return (
<View>
<Button title="Share photo" onPress={addPhoto} />
{progress.percent !== null ? (
<Text>{Math.round(progress.percent)}% uploaded</Text>
) : null}
<FlatList
data={photos}
keyExtractor={(photo) => photo.id}
renderItem={({ item }) => (
<View>
<ImgwireImage
id={item.id}
width={900}
height={600}
resizing_type="cover"
gravity="attention"
format="auto"
style={{ width: '100%', height: 220 }}
/>
<Text>{item.caption}</Text>
</View>
)}
/>
</View>
);
}
The React Native SDK uploads the local file URI and renders an Imgwire CDN variant for the feed.
Save the image ID to your backend
In a real photo sharing app, save the Imgwire image ID with the post record after upload:
await fetch('https://api.example.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
caption,
imageId: image.id,
}),
});
Your backend can later return the image ID to other clients, generate server-side URLs, moderate the post, or delete images when posts are removed.
Render existing posts
When your API returns posts, render the stored image IDs:
type Post = {
id: string;
caption: string;
imageId: string;
};
function PostCard({ post }: { post: Post }) {
return (
<View>
<ImgwireImage
id={post.imageId}
width={1200}
height={800}
resizing_type="cover"
gravity="attention"
format="auto"
style={{ width: '100%', height: 260 }}
/>
<Text>{post.caption}</Text>
</View>
);
}
Use consistent dimensions for feed cards so the layout does not jump while images load.
Best practices
- Use signed uploads for accounts, private workspaces, or paid user flows.
- Store Imgwire image IDs with post records.
- Render feed images at the dimensions your UI actually needs.
- Use
gravity=attentionfor user-generated photos with varied composition. - Keep upload progress visible for large mobile uploads and poor network conditions.
Related pages
- Frontend Quickstart
- Building a Next.js API endpoint to issue upload tokens with Imgwire
- Responsive image variants using srcset
- Improving Website Performance
Last updated at: May 8, 2026