Camera Apps With react-native-vision-camera: The v5 Guide (2026)

react-native-vision-camera 5.2.3 explained: Nitro setup, devices, photo, video and frame outputs, controller, barcode scanning, migration from v4.

Camera Apps With react-native-vision-camera (v5)

The camera is the one part of a phone app where the platform difference refuses to hide. AVFoundation and CameraX do not agree on what a device is, what a resolution means, or when a frame is yours to keep. react-native-vision-camera exists to paper over that, and version 5 papers over it with a different architecture than the one most tutorials still describe. If you learned VisionCamera from a 2023 post, the imports have changed, useFrameProcessor has a new name, and the whole thing runs on Nitro Modules. This guide is the current shape, pinned to react-native-vision-camera 5.2.3 (released August 20, 2026, MIT) and written against the 5.x documentation.

Read it as a map of the moving parts: devices, outputs, sessions, controllers, and the barcode scanner that used to be a prop and is now its own package. The code samples are as the docs describe them for 5.2.3, and where I have opinions I have marked them as mine.

What react-native-vision-camera is in 2026

VisionCamera is a native camera framework by Marc Rousavy and Margelo. It uses AVFoundation on iOS and CameraX on Android, and since version 5 it is built on react-native-nitro-modules, which means JavaScript talks to native objects directly instead of through the bridge. Photo and video capture, QR and barcode scanning, multi camera, 4K and 8K stills, 30 to 240 FPS, HDR and night modes, frame streaming to JavaScript worklets, and a GPU accelerated resizer. Version 4 is no longer actively maintained, per the README.

The architectural change worth understanding before writing a line: everything is an object. A CameraDevice is an input. A CameraOutput (photo, video, frame, depth, preview) is a destination. A CameraSession connects inputs to outputs, and its configure() returns a CameraController you use to zoom, focus and adjust exposure. The <Camera /> component is a convenience wrapper around that session. So is the useCamera() hook. You can also skip both and drive the session imperatively. The docs show all three styles side by side for every feature, which I appreciate more every time I have to debug one.

Installation

npm install react-native-vision-camera react-native-nitro-modules react-native-nitro-image
cd ios && pod install

Both Nitro packages are peer dependencies in 5.2.3 and the install is not optional. react-native-nitro-image is the image type the library hands you for previews and still image scanning. Expo projects need a development build or prebuild; VisionCamera does not run in Expo Go. If you are choosing between this and expo-camera (57.0.5 as of this month), Margelo’s own comparison page is honest: pick Expo Camera for Expo Go, web and fast setup; pick VisionCamera for camera first apps, pro controls, realtime frames and ML, depth, HDR and RAW, multi cam and native plugins. We covered where Expo fits in general in What is Expo.

Add the platform permission strings: NSCameraUsageDescription (and NSMicrophoneUsageDescription if you record audio) in Info.plist, and android.permission.CAMERA (plus RECORD_AUDIO) in the manifest. Expo users do the same through the config plugin.

Permission and a first preview

import React, { useEffect } from 'react'
import { Camera, useCameraPermission } from 'react-native-vision-camera'

export function App() {
  const { hasPermission, requestPermission } = useCameraPermission()

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

  if (!hasPermission) return null

  return (
    <Camera
      style={{ flex: 1 }}
      isActive={true}
      device="back"
    />
  )
}

Two things are new here compared with v3 and v4. device accepts the string "back" directly, so the two line useCameraDevices dance is gone for the common case. And isActive is still the single most important prop in the library: tie it to screen focus (useIsFocused from React Navigation) so the camera releases when the user navigates away. A camera left active in a background tab is the number one battery complaint in camera app reviews and it is one boolean.

Devices: picking the lens

When the default is not enough, useCameraDevice gives you a real CameraDevice object that reports what it can do.

const device = useCameraDevice('back', {
  physicalDevices: ['ultra-wide-angle', 'wide-angle', 'telephoto'],
})

console.log(device?.getSupportedResolutions('video'))
console.log(device?.supportedFPSRanges)
console.log(device?.supportsPhotoHDR)
console.log(device?.supportsVideoStabilizationMode('cinematic'))

Asking for three physical devices gets you a virtual “triple” camera on phones that have one, which is what makes smooth zoom from 0.5x to 3x possible without a visible lens switch. Iterate device.physicalDevices to see what you were actually given. On Android the set of virtual devices is narrower than iOS and depends on the OEM’s CameraX support, so query rather than assume.

The imperative form exists too: await VisionCamera.createDeviceFactory() then deviceFactory.getDefaultCamera('back'). You will want it in a background service or a test, not in a component.

Outputs: photo, video, frames

Outputs are created with hooks and passed to the camera as an array. Nothing captures until an output is attached.

Photos

import { Camera, usePhotoOutput, CommonResolutions } from 'react-native-vision-camera'

const photoOutput = usePhotoOutput({
  targetResolution: CommonResolutions.UHD_16_9,
  previewImageTargetSize: { width: 100, height: 150 },
})

<Camera style={{ flex: 1 }} isActive device="back" outputs={[photoOutput]} />

async function shoot() {
  const photo = await photoOutput.capturePhoto(
    {},
    {
      onWillCapturePhoto: () => playShutterSound(),
      onPreviewImageAvailable: (image) => setThumbnail(image),
    }
  )
  try {
    // use photo.width, photo.height, encode or upload
  } finally {
    photo.dispose()
  }
}

Two details that bite. First, capturePhoto returns an in memory Photo object, and the docs are explicit that you must call dispose() when you are done, otherwise the JS runtime may hold native memory long enough to stall the camera. If you just want a file, capturePhotoToFile() returns { filePath } and you skip the memory management entirely. Second, the capture callbacks (onWillBeginCapture, onWillCapturePhoto, onDidCapturePhoto) are how you get a snappy shutter: play the sound and flash the screen on onWillCapturePhoto, not when the promise resolves, which on a 48 megapixel HDR capture can be a noticeable moment later. The preview image callback gives you a thumbnail before the full photo is processed, which is what a camera roll strip wants.

Video

const videoOutput = useVideoOutput({ enableAudio: true })

<Camera style={{ flex: 1 }} isActive device="back" outputs={[photoOutput, videoOutput]} />

Audio is off by default and turning it on requires the microphone permission. A photo output and a video output can be attached at the same time, which is how you offer a still capture button during recording. By default an active recording stops when the input device changes (a lens switch during zoom); the docs describe a persistent video output option for continuing across that, which is what you want for a camera app and what you do not want for a document scanner.

Frames: the old frame processors

This is the rename. useFrameProcessor is now useFrameOutput, and the frame stream is just another output.

import { useFrameOutput } from 'react-native-vision-camera'

const frameOutput = useFrameOutput({
  pixelFormat: 'yuv',
  onFrame(frame) {
    'worklet'
    try {
      // frame.width, frame.height, frame.pixelFormat === 'yuv-420-8-bit-full'
      runMyModel(frame)
    } finally {
      frame.dispose()
    }
  },
})

The callback is a worklet, it runs on a camera thread, and it must dispose the frame. The docs put frame.dispose() in a finally for a reason: a thrown error inside the handler that skips disposal leaks a native buffer per frame, and at 30 FPS that is a crash within seconds. Request yuv when your model wants it; the default format depends on the platform. For the imperative path you create the output, spin up a worklet runtime for frameOutput.thread, and set the callback from inside that runtime, which is also how you would run frames on a runtime that now has its own fetch after this month’s react-native-worklets 0.13.0.

If you want to draw on frames rather than analyse them, <SkiaCamera /> renders the stream through a Skia canvas and 5.2.0 added a targetResolution prop to it. That pairs with everything in our Skia guide, including the caveat that Skia 2.13.0 dropped the legacy architecture last week.

Controller: zoom, focus, exposure

Live controls go through a CameraController, which becomes available once the session is configured.

import { useRef } from 'react'
import { Camera, type CameraRef } from 'react-native-vision-camera'

const camera = useRef<CameraRef>(null)

<Camera
  ref={camera}
  style={{ flex: 1 }}
  isActive
  device="back"
  onConfigured={async () => {
    const controller = camera.current?.controller
    if (controller?.device.supportsExposureBias) {
      const target = Math.min(1.5, controller.device.maxExposureBias)
      await controller.setExposureBias(target)
    }
  }}
  onInterruptionStarted={(reason) => console.log('interrupted', reason)}
  onInterruptionEnded={() => console.log('resumed')}
/>

Zoom, tap to focus (a focus metering action at a point, with adaptiveness: 'locked' supported on Android since 5.1.1), exposure bias and AE/AF/AWB locks all live here. Read the controller’s device capabilities before calling, and clamp to the reported ranges, because the native layer will reject out of range values rather than clamp for you. The interruption callbacks fire when the system takes the camera away (a phone call, a Split View on iPad, another app on Android). Handle them: hide the shutter, show a message, and do not try to capture until onInterruptionEnded.

Barcode and QR scanning

Scanning moved to its own package, react-native-vision-camera-barcode-scanner, backed by MLKit on both platforms. It gives you three entry points depending on how much control you want.

// 1. The simplest: a self contained view
import { CodeScanner } from 'react-native-vision-camera-barcode-scanner'

<CodeScanner
  isActive={isFocused}
  barcodeFormats={['qr-code', 'ean-13']}
  onBarcodeScanned={(barcodes) => handle(barcodes[0])}
  onError={(e) => console.error(e)}
/>

// 2. As an output on your own Camera
const barcodeOutput = useBarcodeScannerOutput({
  barcodeFormats: ['qr-code'],
  onBarcodeScanned(barcodes) { handle(barcodes[0]) },
  onError(error) { console.error(error) },
})
<Camera style={{ flex: 1 }} isActive device="back" outputs={[barcodeOutput]} />

// 3. Inside a frame output, alongside your own processing
const scanner = useBarcodeScanner({ barcodeFormats: ['qr-code'] })
const frameOutput = useFrameOutput({
  onFrame(frame) {
    'worklet'
    const codes = scanner.scanCodes(frame)
    frame.dispose()
  },
})

Each Barcode exposes format, valueType (url, wifi, contact and so on), rawValue, displayValue and rawBytes as an ArrayBuffer. Restrict barcodeFormats to what you actually accept; 'all-formats' is slower and produces more false positives on busy backgrounds. Since 5.1.0 there is also scanCodesInImageAsync for a still image loaded through react-native-nitro-image, which is how you scan a QR code from the photo library. And if all you need is a one shot native scanner sheet with no custom UI, the docs point you at react-native-data-scanner instead, which is the right call for a “scan to pay” flow with no camera UI of your own.

Performance and battery, the short version

  • isActive off when the screen is not focused. Non negotiable.
  • Match targetResolution to the job. A barcode scanner at 4K is wasted bandwidth; 1080p is plenty.
  • Dispose every Frame and every Photo. Use finally.
  • Keep worklets small. Anything heavy goes to a native plugin or a GPU resizer first; 5.2.0 added a 'stretch' scale mode to the resizer.
  • Request the pixel format your model wants instead of converting per frame.
  • Use the preview image callback for thumbnails rather than decoding the full photo on the JS thread.

Migrating from v3 or v4

The mental model changes more than the surface. Devices are now objects you query rather than a list you filter; outputs are explicit rather than photo={true} and video={true} props; the frame processor hook is useFrameOutput and takes an options object with onFrame; the code scanner left the core package; and controls moved from methods on the camera ref to a controller on it. Install the two Nitro peers, run the app on a device (the simulator has no camera on either platform), and expect to spend most of the migration in whatever code touched frames. Margelo maintains a migration guide on the docs site, which I would read before touching a large app.

FAQ

Does react-native-vision-camera work with Expo? Yes, with a development build or prebuild and the config plugin. Not in Expo Go.

Is v4 still supported? The README states that v4 is no longer actively maintained now that v5 is out.

Do I need react-native-worklets? Frame outputs run as worklets, and the imperative frame API creates a worklet runtime for the output thread. The barcode scanner and plain photo or video capture work without writing a worklet yourself.

Can I test on a simulator? Neither the iOS simulator nor the Android emulator exposes a real camera to the library in a useful way. Test capture on hardware; test your UI states on the simulator with the camera inactive.

VisionCamera or expo-camera? expo-camera for basic capture in an Expo app that also targets web or Expo Go. VisionCamera when you need frame access, multi cam, pro controls, or native plugins.

Leave a Reply 0

Your email address will not be published. Required fields are marked *