expo-camera & expo-image-picker
Capture flows, permissions, and memory-safe previews - the camera cookbook for Expo SDK 57 apps that attach photos to forms, profiles, and field reports.
Search across all documentation pages
Capture flows, permissions, and memory-safe previews - the camera cookbook for Expo SDK 57 apps that attach photos to forms, profiles, and field reports.
Quick-reference recipe card - copy-paste ready.
npx expo install expo-camera expo-image-picker// app.config.ts
export default {
plugins: [
[
"expo-camera",
{
cameraPermission:
"Allow InspectPro to photograph equipment for work orders.",
microphonePermission:
"Allow InspectPro to record audio with video inspections.",
recordAudioAndroid: true,
},
],
[
"expo-image-picker",
{
photosPermission:
"Allow InspectPro to attach existing photos from your library.",
},
],
],
};import * as ImagePicker from "expo-image-picker";
export async function pickImageFromLibrary() {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== "granted") return null;
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: true,
quality: 0.7,
exif: false,
});
if (result.canceled) return null;
return result.assets[0];
}When to reach for this:
expo-camera (BarcodeScanner module).When to avoid:
Camera preview with permission gate, library fallback, and downscaled preview URI.
npx expo install expo-camera expo-image-picker expo-image-manipulator// src/media/CaptureSheet.tsx
import { useRef, useState } from "react";
import { Image, Pressable, Text, View } from "react-native";
import { CameraView, useCameraPermissions } from "expo-camera";
import * as ImageManipulator from "expo-image-manipulator";
import * as ImagePicker from "expo-image-picker";
async function pickImageFromLibrary() {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== "granted") return null;
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: true,
quality: 0.7,
});
if (result.canceled) return null;
return result.assets[0];
}
type Props = {
onCaptured: (uri: string) => void;
};
export function CaptureSheet({ onCaptured }: Props) {
const cameraRef = useRef<CameraView>(null);
const [permission, requestPermission] = useCameraPermissions();
const [previewUri, setPreviewUri] = useState<string | null>(null);
if (!permission?.granted) {
return (
<View style={{ padding: 24, gap: 12 }}>
<Text>Photograph equipment labels for audit trails.</Text>
<Pressable onPress={requestPermission}>
<Text style={{ color: "#2563eb" }}>Allow camera</Text>
</Pressable>
<Pressable
onPress={async () => {
const asset = await pickImageFromLibrary();
if (asset) await setDownscaledPreview(asset.uri);
}}
>
<Text>Choose from library instead</Text>
</Pressable>
</View>
);
}
async function setDownscaledPreview(uri: string) {
const manipulated = await ImageManipulator.manipulateAsync(
uri,
[{ resize: { width: 1280 } }],
{ compress: 0.75, format: ImageManipulator.SaveFormat.JPEG }
);
setPreviewUri(manipulated.uri);
}
async function takePhoto() {
const photo = await cameraRef.current?.takePictureAsync({
quality: 0.8,
skipProcessing: false,
});
if (photo?.uri) await setDownscaledPreview(photo.uri);
}
return (
<View style={{ flex: 1 }}>
{previewUri ? (
<View style={{ flex: 1, gap: 12, padding: 16 }}>
<Image
source={{ uri: previewUri }}
style={{ flex: 1, borderRadius: 8 }}
resizeMode="contain"
/>
<Pressable onPress={() => onCaptured(previewUri)}>
<Text style={{ fontWeight: "600" }}>Use photo</Text>
</Pressable>
<Pressable onPress={() => setPreviewUri(null)}>
<Text>Retake</Text>
</Pressable>
</View>
) : (
<CameraView ref={cameraRef} style={{ flex: 1 }} facing="back">
<Pressable onPress={takePhoto} style={{ margin: 24, padding: 16 }}>
<Text style={{ color: "#fff" }}>Capture</Text>
</Pressable>
</CameraView>
)}
</View>
);
}What this demonstrates:
useCameraPermissions hook for camera gate with library fallback.takePictureAsync with quality cap before manipulator pass.expo-image-manipulator resize to 1280px width - memory-safe preview.| Module | Best for | Permission |
|---|---|---|
expo-camera | Live preview, barcode scan, video | Camera (+ mic for video) |
expo-image-picker | Gallery / system picker UI | Photos library |
| Both | Avatar flows with "Take photo" + "Choose existing" | Request only what user selects |
Risky: store full 4032×3024 URI in React state + render <Image />
Safer: manipulateAsync → max width 1280, JPEG 0.75
Upload: separate full-res file on disk if backend requires - stream via FileSystemImage decodes full bitmap - downscale before preview.previewUri state after upload completes - release file references.expo-file-system File for upload bodies - see ./expo-file-system.md.const video = await cameraRef.current?.recordAsync({
maxDuration: 60,
});File.createUploadTask.<CameraView
barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
onBarcodeScanned={({ data }) => handleScan(data)}
/>onBarcodeScanned fires rapidly.| API | Module | Purpose |
|---|---|---|
CameraView | expo-camera | Live preview component |
useCameraPermissions | expo-camera | Permission hook |
takePictureAsync | expo-camera | Still capture |
launchImageLibraryAsync | image-picker | Gallery pick |
launchCameraAsync | image-picker | System camera intent |
manipulateAsync | image-manipulator | Resize/compress |
<Image> render.launchImageLibraryAsync fails. Fix: requestMediaLibraryPermissionsAsync.| Alternative | Use When | Don't Use When |
|---|---|---|
expo-camera | Custom overlay, scanning | Simple one-shot attach |
launchCameraAsync | Native camera UX suffices | Custom viewfinder branding |
expo-document-picker | PDFs and documents | Photos only |
| Native vision-camera | Advanced frame processors | Standard capture flows |
npx expo install expo-camera expo-image-pickerAdd config plugin permission strings, then rebuild if native permissions changed.
expo-file-system File.createUploadTask or fetch with File blob body.Platform.OS and offer file input on web.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026