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:
-
AVCaptureSessionsetup andstartRunning()run on a dedicated backgroundDispatchQueue— Apple's docs are explicit thatstartRunning()blocks and must not run on the main thread - Metadata callbacks run on
DispatchQueue.main— lightweight string reads, no extra context switch needed - All
NotificationCenterposts toQrCameraProViewhappen on main (UIKit requirement)
Android:
-
bindToLifecycleruns 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);
});
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
Native view
<QrCameraProView style={{ flex: 1 }} />
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
iOS — add to Info.plist:
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) needs camera access to scan QR codes.</string>
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
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.