DEV Community

Cover image for Building a Native QR/Barcode Scanner for React Native — New Architecture Ready
Rushikesh Pandit
Rushikesh Pandit

Posted on

Building a Native QR/Barcode Scanner for React Native — New Architecture Ready

Most QR scanner libraries for React Native share the same problems — they're unmaintained, they don't support the New Architecture, or they pull in a full camera SDK for what is a single-feature module. I wanted something lean, production-grade, and built the right way. So I built it.

This is react-native-qr-camera-pro — a QR and barcode scanner for React Native built entirely with native code. No JavaScript frame processing. No unnecessary dependencies. Swift on iOS, Kotlin on Android, TurboModules and Fabric throughout.


Why Native-Only?

The common alternative is running frame analysis in JavaScript — grabbing frames via a JS-accessible camera API and running a WASM or JS barcode decoder on them. It works, but it puts real pressure on the JS thread and limits your frame rate.

With native-only processing:

  • iOS uses AVCaptureMetadataOutput — Apple's own pipeline for detecting machine-readable codes. Frames are never copied to user space; the kernel hands off a reference to the same buffer.
  • Android uses CameraX ImageAnalysis + ML Kit — Google's on-device barcode scanner backed by hardware-accelerated inference where available.

The JS bridge is touched at most once every 500ms to deliver a result. Everything else stays native.


Architecture

The module is three layers:

Architecture

The module is three layers, each with a single responsibility:

Layer What it does
JavaScript / TypeScript Public API — QrCameraProView, startScanning(), stopScanning(), toggleTorch(), useBarcodeScanner(), useCameraError()
Native Bridge (TurboModules + Fabric) Type-safe JSI communication between JS and native. Codegen spec drives both the iOS C++ adapter and the Android Kotlin stub.
Native Platform (iOS + Android) All camera and barcode logic. AVFoundation on iOS, CameraX + ML Kit on Android. Zero JS involvement in frame processing.

iOS (Swift)

Class Responsibility
QrCameraProSwift Owns the AVCaptureSession lifecycle
BarcodeThrottler Throttle + dedup logic
BarcodeTypeMapper Maps AVMetadataObject.ObjectType → JS string
QrCameraProView Hosts AVCaptureVideoPreviewLayer
QrCameraProViewManager Vends view instances to Fabric

Android (Kotlin)

Class Responsibility
QrCameraProModule Camera session lifecycle + event emission
BarcodeThrottler Throttle + dedup logic (same contract as iOS)
BarcodeFormatMapper Maps ML Kit FORMAT_* int → JS string
BarcodeAnalyzer CameraX frame analysis + ML Kit client
QrCameraProView Hosts CameraX PreviewView
QrCameraProViewManager Vends view instances to Fabric

TurboModules handle all imperative calls (startScanning, stopScanning, toggleTorch) and event emission (onBarcodeScanned, onCameraError) via JSI — no JSON serialisation, no async queue hop.

Fabric handles the camera preview component. The preview surface (AVCaptureVideoPreviewLayer on iOS, CameraX PreviewView on Android) is a native sublayer that React Native sizes and positions. The preview frame rate is fully decoupled from the JS render cycle.


Threading

Getting threads right is the most important part of a camera module.

iOS:

  • AVCaptureSession setup and startRunning() run on a dedicated background DispatchQueue — Apple's docs are explicit that startRunning() blocks and must not run on the main thread
  • Metadata callbacks run on DispatchQueue.main — lightweight string reads, no extra context switch needed
  • All NotificationCenter posts to QrCameraProView happen on main (UIKit requirement)

Android:

  • bindToLifecycle runs on the UI thread (CameraX requirement)
  • Frame analysis runs on a dedicated single-thread ExecutorService — keeps the main thread clear
  • ML Kit results are handled on the same executor thread before being forwarded to JS

Scan Throttling

AVCaptureMetadataOutput and ML Kit can fire multiple times per second for a single held barcode. Forwarding every detection to JavaScript would saturate the bridge.

Both platforms implement a BarcodeThrottler that gates emissions:

  • Same code within 500ms → suppressed
  • Same code after 500ms → allowed (intentional re-scan)
  • Different code → always allowed
  • Throttle state resets on every stopScanning() call

The 500ms default keeps the bridge load minimal while remaining imperceptible to users.


The JS API

Hooks

// Subscribe to scan results
useBarcodeScanner((barcode: Barcode) => {
  console.log(barcode.data, barcode.type);
});

// Subscribe to camera errors
useCameraError((error: CameraErrorEvent) => {
  Alert.alert('Camera Error', error.message);
});
Enter fullscreen mode Exit fullscreen mode

Both hooks subscribe once on mount and unsubscribe once on unmount — regardless of how many times the parent re-renders or passes a new callback reference. The latest callback is always called via a ref. This keeps the native listener count stable at 1 for the lifetime of the component.

Imperative API

startScanning();   // starts the session, requests permission on Android
stopScanning();    // stops the session, releases camera hardware
toggleTorch(true); // torch on
toggleTorch(false);// torch off
Enter fullscreen mode Exit fullscreen mode

Native view

<QrCameraProView style={{ flex: 1 }} />
Enter fullscreen mode Exit fullscreen mode

Renders AVCaptureVideoPreviewLayer on iOS and CameraX PreviewView on Android. Style it like any other View.


Supported Formats

Type string Format
QR_CODE QR Code
EAN_8 EAN-8
EAN_13 EAN-13
PDF_417 PDF-417
AZTEC Aztec
CODE_128 Code 128
CODE_39 Code 39
CODE_93 Code 93
DATA_MATRIX Data Matrix
ITF Interleaved 2 of 5
ITF_14 ITF-14
UPC_E UPC-E

Installation

yarn add react-native-qr-camera-pro
# or
npm install react-native-qr-camera-pro
Enter fullscreen mode Exit fullscreen mode

iOS — add to Info.plist:

<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) needs camera access to scan QR codes.</string>
Enter fullscreen mode Exit fullscreen mode

Then cd ios && pod install.

Android — the CAMERA permission is declared in the library manifest and merged automatically. No manual edits needed.


Use Cases

  • Retail / inventory — scan product barcodes for stock checks
  • Event check-in — verify QR-coded tickets at entry
  • Logistics — track packages by scanning shipping labels
  • Payments — read QR codes for payment flows
  • Authentication — QR-based 2FA or login

What's Next

  • Scan region / viewfinder crop
  • Scan success haptic feedback
  • Expo plugin

If you're building anything on React Native that needs reliable, fast barcode scanning without pulling in a full camera SDK — give it a try. Feedback welcome, especially from anyone hitting edge cases on specific devices or Android versions.

npm Package
GitHub Repository
Full Documentation
Issue Tracker

Found this helpful? Drop a ❤️ on the article and ⭐ on GitHub!
Questions or suggestions? Drop them in the comments below.

Feel free to reach out:
LinkedIn: https://www.linkedin.com/in/rushikesh-pandit-646834100/
GitHub: https://github.com/rushikeshpandit
Portfolio: https://www.rushikeshpandit.in

ReactNative #TypeScript #MobileDevelopment #SoftwareEngineering #iOS #Android #Swift #Kotlin #OpenSource #TurboModules #CameraX #AVFoundation

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.