PEAKIQ - Software Solutions & Digital Innovation Peakiq Software Development

Peakiq Blog

Building a Reusable Vision Camera Component in React Native

Learn to build a versatile, ref-forwarded camera component in React Native using react-native-vision-camera for photo, video, and barcode scanning in one

Editorial6 min read1147 words
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

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>;
}
  • 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;
}

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;

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] : []),
];

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);

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);

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 />}
    />
  );
};

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.