DEV Community

Cover image for React Native Vision Camera: Reusable Component Guide
PEAKIQ
PEAKIQ

Posted on • Originally published at peakiq.in

React Native Vision Camera: Reusable Component Guide

Originally published on PEAKIQ

Source: https://www.peakiq.in/blog/building-a-reusable-vision-camera-component-in-react-native


Building a Reusable Vision Camera Component in React Native

react-native-vision-camera is the go-to camera library for React Native in 2025 — but wiring up photo capture, video recording, and barcode scanning in every screen gets repetitive fast.

This guide walks through building a single reusable VisionCamera component that handles all three, exposed via a clean ref API. Published on peakiq.in.


What We're Building

A single VisionCamera component that:

  • Takes photos with configurable flash
  • Records video with a start/stop ref API
  • Optionally scans barcodes in real time
  • Accepts an overlay slot for custom UI
  • Clamps zoom between device min and max
  • Handles missing camera devices gracefully

Dependencies

npm install react-native-vision-camera react-native-vision-camera-barcode-scanner
Enter fullscreen mode Exit fullscreen mode

Make sure you have camera and microphone permissions configured in both AndroidManifest.xml and Info.plist.


The Handle Interface

We expose three imperative methods via forwardRef so the parent screen can control the camera without prop drilling:

export interface VisionCameraHandle {
  takePhoto: (options?: { flash?: 'on' | 'off' | 'auto' }) => Promise<string>;
  recordAsync: () => Promise<{ uri: string; codec: string }>;
  stopRecording: () => Promise<void>;
}
Enter fullscreen mode Exit fullscreen mode
  • takePhoto resolves with the file path of the captured image
  • recordAsync starts recording and resolves with the video URI when stopped
  • stopRecording signals the recorder to finalize and save

Component Props

interface Props {
  style?: StyleProp<ViewStyle>;
  zoom?: number;
  maxZoom?: number;
  isActive?: boolean;
  overlay?: React.ReactNode;
  flash?: 'on' | 'off' | 'auto';
  onMountError?: (error: Error) => void;
  onCameraReady?: () => void;
  onBarCodeRead?: (barcode: { value: string; type: string }) => void;
  enableBarcode?: boolean;
  enableVideo?: boolean;
}
Enter fullscreen mode Exit fullscreen mode

enableBarcode and enableVideo are opt-in — outputs are only registered when needed, which keeps the camera pipeline lean.


Full Implementation

import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { View, StyleProp, ViewStyle, Text, StyleSheet } from 'react-native';
import {
  Camera,
  useCameraDevice,
  usePhotoOutput,
  useVideoOutput,
} from 'react-native-vision-camera';
import { useBarcodeScannerOutput } from 'react-native-vision-camera-barcode-scanner';

export interface VisionCameraHandle {
  takePhoto: (options?: { flash?: 'on' | 'off' | 'auto' }) => Promise<string>;
  recordAsync: () => Promise<{ uri: string; codec: string }>;
  stopRecording: () => Promise<void>;
}

interface Props {
  style?: StyleProp<ViewStyle>;
  zoom?: number;
  maxZoom?: number;
  isActive?: boolean;
  overlay?: React.ReactNode;
  flash?: 'on' | 'off' | 'auto';
  onMountError?: (error: Error) => void;
  onCameraReady?: () => void;
  onBarCodeRead?: (barcode: { value: string; type: string }) => void;
  enableBarcode?: boolean;
  enableVideo?: boolean;
}

const VisionCamera = forwardRef<VisionCameraHandle, Props>(
  (
    {
      style,
      zoom = 1,
      maxZoom,
      isActive = true,
      overlay,
      flash = 'off',
      onMountError,
      onCameraReady,
      onBarCodeRead,
      enableBarcode = false,
      enableVideo = false,
    },
    ref,
  ) => {
    const device = useCameraDevice('back');
    const photoOutput = usePhotoOutput();
    const videoOutput = useVideoOutput({ enableAudio: enableVideo });
    const recorderRef = useRef<any>(null);

    const barcodeOutput = useBarcodeScannerOutput({
      barcodeFormats: ['all-formats'],
      onBarcodeScanned: (barcodes) => {
        if (!enableBarcode) return;
        const first = barcodes[0];
        if (first) {
          onBarCodeRead?.({
            value: first.rawValue ?? '',
            type: first.format ?? '',
          });
        }
      },
      onError: (error) => {
        console.warn('Barcode scanner error', error);
      },
    });

    useImperativeHandle(
      ref,
      () => ({
        takePhoto: async (options) => {
          const { filePath } = await photoOutput.capturePhotoToFile(
            { flashMode: options?.flash ?? flash },
            {},
          );
          return filePath;
        },

        recordAsync: () => {
          return new Promise(async (resolve, reject) => {
            try {
              const recorder = await videoOutput.createRecorder({});
              recorderRef.current = recorder;

              await recorder.startRecording(
                (filePath, reason) => {
                  resolve({ uri: `file://${filePath}`, codec: 'mp4' });
                },
                (error) => {
                  reject(error);
                },
              );
            } catch (e) {
              reject(e);
            }
          });
        },

        stopRecording: async () => {
          try {
            await recorderRef.current?.stopRecording();
            recorderRef.current = null;
          } catch (error) {
            console.info(error);
          }
        },
      }),
      [photoOutput, videoOutput, flash],
    );

    if (!device) {
      return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
          <Text>No camera device found</Text>
        </View>
      );
    }

    const resolvedMaxZoom = maxZoom ?? device.maxZoom;
    const clampedZoom = Math.min(Math.max(zoom, device.minZoom), resolvedMaxZoom);

    const outputs = [
      photoOutput,
      ...(enableVideo ? [videoOutput] : []),
      ...(enableBarcode ? [barcodeOutput] : []),
    ];

    return (
      <View style={{ flex: 1 }}>
        <Camera
          style={[StyleSheet.absoluteFill, style]}
          device={device}
          isActive={isActive}
          zoom={clampedZoom}
          outputs={outputs}
          onStarted={onCameraReady}
          onError={(error) => onMountError?.(new Error(error.message))}
        />
        {overlay}
      </View>
    );
  },
);

VisionCamera.displayName = 'VisionCamera';

export default VisionCamera;
Enter fullscreen mode Exit fullscreen mode

Key Design Decisions

Conditional outputs array

Outputs are only added to the camera pipeline when the corresponding feature is enabled:

const outputs = [
  photoOutput,
  ...(enableVideo ? [videoOutput] : []),
  ...(enableBarcode ? [barcodeOutput] : []),
];
Enter fullscreen mode Exit fullscreen mode

This avoids unnecessary processing — a photo-only screen won't pay the cost of video or barcode initialization.

Zoom clamping

Zoom is clamped between the device's reported minZoom and maxZoom values, with an optional maxZoom prop override:

const resolvedMaxZoom = maxZoom ?? device.maxZoom;
const clampedZoom = Math.min(Math.max(zoom, device.minZoom), resolvedMaxZoom);
Enter fullscreen mode Exit fullscreen mode

Recorder ref

The active recorder instance is stored in a useRef so stopRecording can access it without closing over stale state:

const recorderRef = useRef<any>(null);
Enter fullscreen mode Exit fullscreen mode

Usage Example

import { useRef } from 'react';
import VisionCamera, { VisionCameraHandle } from './VisionCamera';

const ScannerScreen = () => {
  const cameraRef = useRef<VisionCameraHandle>(null);

  const handleCapture = async () => {
    const path = await cameraRef.current?.takePhoto({ flash: 'auto' });
    console.log('Photo saved at:', path);
  };

  return (
    <VisionCamera
      ref={cameraRef}
      enableBarcode
      onBarCodeRead={({ value, type }) => console.log(type, value)}
      onCameraReady={() => console.log('Camera ready')}
      overlay={<ScannerOverlay />}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

What to Build Next

  • Add front camera support by accepting a cameraPosition prop ('back' | 'front')
  • Expose zoom gesture handling via react-native-gesture-handler
  • Add a captureMode prop to switch between photo and video mode declaratively
  • Integrate with a global media store to manage captured files

Published on peakiq.in — React Native guides, component patterns, and tutorials.

Top comments (0)