<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/react-native-vision-camera-vs-expo-camera-vs-expo-image-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/react-native-vision-camera-vs-expo-camera-vs-expo-image-2026/raw.md -->
<!-- Source path: content/guides/react-native-vision-camera-vs-expo-camera-vs-expo-image-2026.mdx -->

---
og_image: "/images/guides/react-native-vision-camera-vs-expo-camera-vs-expo-image-2026.webp"
title: Vision Camera vs Expo Camera vs ImagePicker (2026)
description: "react-native-vision-camera vs Expo Camera vs Expo ImagePicker compared for camera and media in React Native. Frame processors, QR scanning, ML integration."
date: "2026-03-09"
author: "PkgPulse Team"
tags: ["react-native", "mobile", "expo", "typescript"]
---

# React Native Vision Camera vs Expo Camera vs Expo ImagePicker 2026

## TL;DR

Camera integration in React Native is layered: you need different tools for different camera use cases. **react-native-vision-camera** (VisionCamera) is the high-performance camera library — frame processors run on the GPU thread, enabling real-time ML inference, QR scanning, barcode detection, and custom computer vision directly from live camera feed. **Expo Camera** is the SDK-native camera — full camera preview with photo/video capture, simple permissions handling, and built into the Expo ecosystem; perfect for standard camera screens. **Expo ImagePicker** is not a camera itself but a media picker — uses the native OS image picker (photo library + optional camera), ideal when you just need "select a photo from gallery or take a photo." For ML/AI on camera frames: VisionCamera. For a custom camera screen: Expo Camera. For pick-a-photo flows: Expo ImagePicker.

## Key Takeaways

- **VisionCamera frame processors run on GPU thread** — zero JS bridge overhead, real-time 60fps analysis
- **Expo Camera works inside Expo Go** — no bare workflow required for basic camera
- **Expo ImagePicker uses the native OS picker** — no custom camera UI needed for simple use cases
- **VisionCamera requires New Architecture** — JSI/Fabric required for frame processors
- **VisionCamera GitHub stars: 7k+** — the standard for advanced camera work
- **Expo Camera supports QR scanning** — `BarcodeScanner` in basic cases without VisionCamera
- **All three require camera permissions** — `expo-permissions` or platform-native permissions

---

## Camera Use Cases and the Right Tool

```
Use Case → Library
─────────────────────────────────────────────────────
Pick photo from gallery only     → Expo ImagePicker
Take a photo (no custom UI)      → Expo ImagePicker (camera option)
Custom camera UI + photo/video   → Expo Camera
Real-time QR/barcode scan        → Expo Camera (simple) or VisionCamera
Face detection, AR, pose detect  → VisionCamera + frame processor
Document scanning                → VisionCamera + frame processor
Custom ML inference on frames    → VisionCamera + frame processor (JSI)
High-quality video recording     → VisionCamera
Portrait / slow-mo video         → VisionCamera (device format control)
```

---

## Expo ImagePicker: Native OS Picker

Expo ImagePicker opens the native iOS/Android media picker. No custom camera UI, no permissions dialogs to manage beyond the initial ask — the OS handles everything.

### Installation

```bash
npx expo install expo-image-picker
```

### Basic Photo Picking

```typescript
import * as ImagePicker from "expo-image-picker";

export function useImagePicker() {
  const pickImage = async (): Promise<string | null> => {
    // Request permissions (iOS only — Android grants automatically)
    const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
    if (status !== "granted") {
      alert("Camera roll access is required to pick photos.");
      return null;
    }

    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      allowsEditing: true,          // Crop after selection
      aspect: [1, 1],               // Square crop
      quality: 0.8,                 // JPEG compression (0-1)
    });

    if (result.canceled) return null;
    return result.assets[0].uri;
  };

  const takePhoto = async (): Promise<string | null> => {
    const { status } = await ImagePicker.requestCameraPermissionsAsync();
    if (status !== "granted") return null;

    const result = await ImagePicker.launchCameraAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      allowsEditing: true,
      aspect: [4, 3],
      quality: 1,
    });

    if (result.canceled) return null;
    return result.assets[0].uri;
  };

  return { pickImage, takePhoto };
}
```

### Avatar Upload Flow

```tsx
import * as ImagePicker from "expo-image-picker";
import { Image, Pressable, View } from "react-native";

function AvatarPicker({ onUpload }: { onUpload: (uri: string) => void }) {
  const [uri, setUri] = useState<string | null>(null);

  const handlePick = async () => {
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      allowsEditing: true,
      aspect: [1, 1],
      quality: 0.9,
      base64: false,
    });

    if (!result.canceled) {
      const imageUri = result.assets[0].uri;
      setUri(imageUri);
      onUpload(imageUri);
    }
  };

  return (
    <Pressable onPress={handlePick} style={styles.avatarContainer}>
      {uri ? (
        <Image source={{ uri }} style={styles.avatar} />
      ) : (
        <View style={styles.placeholder}>
          <Text>Tap to upload</Text>
        </View>
      )}
    </Pressable>
  );
}
```

### Multiple Image Selection

```typescript
const result = await ImagePicker.launchImageLibraryAsync({
  mediaTypes: ImagePicker.MediaTypeOptions.All,  // Photos and videos
  allowsMultipleSelection: true,                 // iOS 14+, Android
  selectionLimit: 5,                             // Max 5 items
  quality: 0.8,
});

if (!result.canceled) {
  const images = result.assets;  // Array of selected images
  const uris = images.map((img) => img.uri);
}
```

---

## Expo Camera: Custom Camera Screen

Expo Camera provides a React component that renders a live camera preview with controls for capturing photos and videos.

### Installation

```bash
npx expo install expo-camera
```

### Basic Camera Screen

```tsx
import { CameraView, CameraType, useCameraPermissions } from "expo-camera";
import { useState, useRef } from "react";
import { Button, StyleSheet, Text, TouchableOpacity, View } from "react-native";

export function CameraScreen() {
  const [facing, setFacing] = useState<CameraType>("back");
  const [permission, requestPermission] = useCameraPermissions();
  const cameraRef = useRef<CameraView>(null);
  const [photo, setPhoto] = useState<string | null>(null);

  if (!permission) {
    return <View />;
  }

  if (!permission.granted) {
    return (
      <View style={styles.container}>
        <Text>Camera permission is required.</Text>
        <Button onPress={requestPermission} title="Grant Permission" />
      </View>
    );
  }

  const takePhoto = async () => {
    if (!cameraRef.current) return;
    const pic = await cameraRef.current.takePictureAsync({
      quality: 0.9,
      base64: false,
      skipProcessing: false,
    });
    if (pic) setPhoto(pic.uri);
  };

  const toggleFacing = () => {
    setFacing((current) => (current === "back" ? "front" : "back"));
  };

  return (
    <View style={styles.container}>
      <CameraView style={styles.camera} facing={facing} ref={cameraRef}>
        <View style={styles.buttonContainer}>
          <TouchableOpacity onPress={toggleFacing} style={styles.button}>
            <Text style={styles.text}>Flip</Text>
          </TouchableOpacity>
          <TouchableOpacity onPress={takePhoto} style={styles.shutterButton} />
        </View>
      </CameraView>
    </View>
  );
}
```

### QR Code Scanning

```tsx
import { CameraView, useCameraPermissions } from "expo-camera";

function QRScanner({ onScan }: { onScan: (data: string) => void }) {
  const [permission, requestPermission] = useCameraPermissions();
  const [scanned, setScanned] = useState(false);

  if (!permission?.granted) {
    return <Button onPress={requestPermission} title="Allow Camera" />;
  }

  return (
    <CameraView
      style={StyleSheet.absoluteFillObject}
      facing="back"
      onBarcodeScanned={scanned ? undefined : (result) => {
        setScanned(true);
        onScan(result.data);
        // Re-enable after delay
        setTimeout(() => setScanned(false), 2000);
      }}
      barcodeScannerSettings={{
        barcodeTypes: ["qr", "pdf417", "code128", "ean13"],
      }}
    />
  );
}
```

### Video Recording

```typescript
const startRecording = async () => {
  if (!cameraRef.current) return;

  const video = await cameraRef.current.recordAsync({
    maxDuration: 60,    // Max 60 seconds
    mute: false,
    codec: "h264",      // iOS only
  });

  if (video) {
    console.log("Video URI:", video.uri);
  }
};

const stopRecording = () => {
  cameraRef.current?.stopRecording();
};
```

---

## react-native-vision-camera: High-Performance Camera

VisionCamera is built for demanding camera use cases: real-time frame processing, ML inference, custom recording formats, and full camera control.

### Installation

```bash
npx expo install react-native-vision-camera
# Requires Expo bare workflow or React Native CLI
# Also requires New Architecture (Fabric + JSI) for frame processors
```

### Basic Camera View

```tsx
import { Camera, useCameraDevice, useCameraPermission } from "react-native-vision-camera";

export function VisionCameraView() {
  const device = useCameraDevice("back");
  const { hasPermission, requestPermission } = useCameraPermission();

  useEffect(() => {
    if (!hasPermission) requestPermission();
  }, [hasPermission]);

  if (!hasPermission || !device) return null;

  return (
    <Camera
      style={StyleSheet.absoluteFill}
      device={device}
      isActive={true}
      photo={true}       // Enable photo capture
      video={false}
      audio={false}
    />
  );
}
```

### Taking Photos

```typescript
import { Camera, useCameraDevice } from "react-native-vision-camera";
import { useRef } from "react";

function PhotoCamera() {
  const camera = useRef<Camera>(null);
  const device = useCameraDevice("back");

  const takePhoto = async () => {
    const photo = await camera.current?.takePhoto({
      qualityPrioritization: "quality",  // "speed" | "balanced" | "quality"
      enableAutoRedEyeReduction: true,
      enableAutoDistortionCorrection: false,
      flash: "auto",
    });

    if (photo) {
      console.log("Photo path:", photo.path);
      console.log("Width:", photo.width, "Height:", photo.height);
    }
  };

  return (
    <Camera
      ref={camera}
      style={StyleSheet.absoluteFill}
      device={device!}
      isActive={true}
      photo={true}
    />
  );
}
```

### Frame Processors: Real-Time Analysis

Frame processors are JavaScript functions that run on the GPU thread for every camera frame — no JS bridge, no dropped frames.

```typescript
import { Camera, useCameraDevice, useFrameProcessor } from "react-native-vision-camera";
import { useSharedValue, runOnJS } from "react-native-reanimated";

// Frame processor for QR code detection
function QRScannerVision({ onDetect }: { onDetect: (data: string) => void }) {
  const device = useCameraDevice("back");
  const lastScan = useSharedValue<string>("");

  const frameProcessor = useFrameProcessor((frame) => {
    "worklet";
    // Use a Vision Camera plugin for barcode scanning
    // e.g., vision-camera-code-scanner
    const barcodes = scanBarcodes(frame, [BarcodeFormat.QR_CODE]);

    if (barcodes.length > 0) {
      const data = barcodes[0].displayValue ?? "";
      if (data !== lastScan.value) {
        lastScan.value = data;
        runOnJS(onDetect)(data);  // Call JS from UI thread
      }
    }
  }, []);

  return (
    <Camera
      style={StyleSheet.absoluteFill}
      device={device!}
      isActive={true}
      frameProcessor={frameProcessor}
    />
  );
}
```

### ML Model Integration (Real-Time Pose Detection)

```typescript
import { useFrameProcessor } from "react-native-vision-camera";
import { detectPose } from "vision-camera-pose-detection";  // Community plugin

function PoseDetector() {
  const device = useCameraDevice("front");

  const frameProcessor = useFrameProcessor((frame) => {
    "worklet";
    const poses = detectPose(frame);
    // poses contains keypoints: nose, shoulders, elbows, wrists, hips, knees, ankles
    // All running at 60fps on the GPU thread
    console.log("Detected poses:", poses.length);
  }, []);

  return (
    <Camera
      style={StyleSheet.absoluteFill}
      device={device!}
      isActive={true}
      frameProcessor={frameProcessor}
      fps={60}
    />
  );
}
```

### Device Format Selection

```typescript
import { Camera, useCameraDevice, useCameraFormat } from "react-native-vision-camera";

function ProCamera() {
  const device = useCameraDevice("back");
  const format = useCameraFormat(device, [
    { fps: 60 },                          // Prefer 60fps
    { photoResolution: "max" },           // Maximum photo resolution
    { videoResolution: { width: 3840, height: 2160 } },  // 4K video
  ]);

  return (
    <Camera
      style={StyleSheet.absoluteFill}
      device={device!}
      isActive={true}
      format={format}
      photo={true}
      video={true}
      fps={format?.maxFps ?? 30}
      hdr={true}
      lowLightBoost={device?.supportsLowLightBoost ?? false}
    />
  );
}
```

---

## Feature Comparison

| Feature | Expo ImagePicker | Expo Camera | VisionCamera |
|---------|----------------|------------|-------------|
| **Native OS picker** | ✅ | ❌ | ❌ |
| **Custom camera UI** | ❌ | ✅ | ✅ |
| **Frame processors** | ❌ | ❌ | ✅ |
| **Real-time ML** | ❌ | ❌ | ✅ |
| **QR scanning** | ❌ | ✅ Basic | ✅ Advanced |
| **Video recording** | ✅ Pick only | ✅ | ✅ |
| **4K / HDR** | Depends on OS | ❌ | ✅ |
| **Front camera** | ✅ | ✅ | ✅ |
| **Expo Go** | ✅ | ✅ | ❌ |
| **New Architecture req.** | ❌ | ❌ | ✅ (frame proc.) |
| **Setup complexity** | Very low | Low | High |
| **GitHub stars** | (Expo SDK) | (Expo SDK) | 7k+ |

---

## When to Use Each

**Choose Expo ImagePicker if:**
- Users need to select photos from their gallery or take a quick photo
- No custom camera UI is needed — the native OS picker is fine
- You want the fastest path to "upload a profile picture" functionality
- Your app doesn't need live camera preview at all

**Choose Expo Camera if:**
- You need a custom camera UI (shutter button, flip camera, zoom controls)
- Basic QR/barcode scanning within the Expo managed workflow
- Photo and video capture with a camera preview component
- You're still on Expo Go or the managed workflow

**Choose VisionCamera if:**
- Frame-by-frame analysis is needed: QR scanning, face detection, pose estimation, AR
- Running on-device ML models against the live camera feed
- Maximum camera control: RAW capture, HDR, 4K video, slow motion, custom formats
- You're on the New Architecture (Expo bare workflow or React Native CLI)

---

## Privacy Permissions and App Store Compliance

Camera permissions handling is one of the most consequential UX decisions in mobile development — a poorly worded permission prompt or premature permission request results in users denying access permanently, requiring them to navigate to Settings to re-enable it. Apple requires a camera usage description string in `Info.plist` (`NSCameraUsageDescription`) and microphone usage description (`NSMicrophoneUsageDescription` for video recording) that clearly explains why your app needs access. Vague descriptions like "Camera access required" are routinely rejected in App Store review; specific descriptions like "Camera is used to capture product photos for your listings" are approved. For Expo apps, these strings are configured in `app.json` under `ios.infoPlist`. Request permissions only when the user takes an action that requires them — not on app launch. Expo ImagePicker's `requestMediaLibraryPermissionsAsync()` and `requestCameraPermissionsAsync()` should be called immediately before the relevant picker launch, not in a `useEffect` at component mount. VisionCamera's `useCameraPermission()` hook provides `requestPermission()` which should similarly be called in response to a user action.

## Frame Processor Plugin Ecosystem and Custom Native Modules

VisionCamera's frame processor system has an ecosystem of community plugins that provide common computer vision tasks without requiring you to write native code. `vision-camera-code-scanner` wraps Google's ML Kit barcode scanner, supporting 16 barcode formats including QR, EAN-13, Code-128, and PDF417. `vision-camera-face-detector` provides real-time face detection with landmark positions (eyes, nose, mouth) and head pose estimation. `vision-camera-object-detection` runs TensorFlow Lite models for custom object detection. For custom ML workloads, you can write your own frame processor plugin in Swift (iOS) or Kotlin (Android) that receives the raw camera frame buffer and returns structured data to JavaScript — this is how production AR applications and custom scanner apps integrate proprietary on-device models. The frame processor plugin API is JSI-based, meaning plugin execution happens on the UI thread with direct memory access, avoiding serialization overhead. Writing a custom plugin requires native mobile development experience but enables capabilities that no other React Native camera library can match.

## Video Recording Quality, Formats, and Storage

VisionCamera gives the most granular control over video recording parameters. The `useCameraFormat()` hook lets you select the exact recording resolution (up to 4K/8K depending on device), frame rate (including 120fps/240fps slow motion on supported devices), and codec (H.264 or H.265/HEVC). H.265 produces files approximately 40% smaller than H.264 at the same quality, which is significant for apps where users record long videos. Expo Camera's video recording is simpler: `recordAsync()` accepts `maxDuration`, `maxFileSize`, and `mute` parameters but does not expose codec selection or resolution beyond what the platform provides by default. For apps where video quality and file size are critical — fitness apps, educational content, user-generated video platforms — VisionCamera's format control is essential. For apps where users occasionally record short clips and file size is not a primary concern, Expo Camera's simpler API is sufficient. Always test video recording on lower-end Android devices from your target market — hardware capabilities vary significantly, and format selections that work on a Pixel 8 may fail on devices with older camera APIs.

## Migration Path from Expo Camera to VisionCamera

Teams that start with Expo Camera for simplicity and later need frame processors or advanced camera features face a migration to VisionCamera. The migration is not a drop-in replacement — the API surface is significantly different and VisionCamera requires the New Architecture and a bare Expo workflow (or React Native CLI). The migration steps are: eject from Expo managed workflow if necessary, install New Architecture dependencies, replace `CameraView` components with VisionCamera's `Camera` component, rewrite permission handling with `useCameraPermission()`, and rebuild custom camera screens against VisionCamera's props. Photo capture changes from `cameraRef.current.takePictureAsync()` to `cameraRef.current.takePhoto()` with different options structure. The VisionCamera output for photos is a file path rather than a URI, requiring `file://` prefix adjustments in image upload code. Budget two to three days for the migration of a single camera screen, plus additional time for frame processor plugin integration if that is the migration driver. The investment is worthwhile if real-time analysis is genuinely required, but Expo Camera covers the majority of production camera use cases adequately.

## Methodology

Data sourced from the official react-native-vision-camera documentation (mrousavy.com/react-native-vision-camera), Expo Camera documentation (docs.expo.dev/versions/latest/sdk/camera), Expo ImagePicker documentation, GitHub star counts as of February 2026, and community discussions in the Expo Discord and the React Native community on Twitter/X.

---

*Related: [React Native Reanimated vs Moti vs Skia](/guides/react-native-reanimated-vs-moti-vs-skia-animation-2026) for adding animations to your camera UI, or [FlashList vs FlatList vs LegendList](/guides/flashlist-vs-flatlist-vs-legendlist-react-native-lists-2026) for displaying captured photos in a performant gallery.*

*See also: [React vs Vue](/compare/react-vs-vue) and [React vs Svelte](/compare/react-vs-svelte)*
