React Native
Scan barcodes in React Native and Expo apps with qrgen-react-native: native scanning on VisionCamera, or a zero-setup WebView scanner.
Note: This page is generated from
packages/react-native/README.md. Source code:packages/react-native.
Barcode, QR and ID scanning for React Native, part of QRGen (Apache-2.0). Two scanning back ends share one API:
| Component | Engine | Native setup | Best for |
|---|---|---|---|
QRGenScanner |
react-native-vision-camera 4 (ML Kit on Android, AVFoundation on iOS) | Development build (not Expo Go) | Production apps, best speed |
QRGenWebScanner |
Hosted QRGen embed page in react-native-webview | None (works in Expo Go) | Prototypes, every QRGen symbology, zero native code |
Both use the QRGen symbology ids, result shape, options and error codes from the cross-platform specification, so the results look the same as in the JavaScript, iOS, Android, Flutter and Python SDKs.
Install
npm install qrgen-react-native
Expo
QRGenScanner needs a development build
because vision-camera ships native code:
npx expo install react-native-vision-camera@^4.7.3
Pin major version 4: vision-camera 5 has a different API and is not supported yet.
Add the config plugin to app.json / app.config.js. It writes the iOS camera
permission text and the Android CAMERA permission. enableCodeScanner bundles the
ML Kit barcode model (about 2.4 MB) so scanning works the first time the app is
opened, even offline:
{
"expo": {
"plugins": [
[
"react-native-vision-camera",
{
"cameraPermissionText": "$(PRODUCT_NAME) uses the camera to scan barcodes and QR codes.",
"enableCodeScanner": true
}
]
],
"android": {
"permissions": ["android.permission.VIBRATE"]
}
}
}
Then run npx expo prebuild (or eas build).
For QRGenWebScanner, which also works in Expo Go:
npx expo install react-native-webview
Bare React Native
npm install react-native-vision-camera@^4.7.3 # for QRGenScanner
npm install react-native-webview # for QRGenWebScanner (optional)
cd ios && pod install
iOS ios/<App>/Info.plist:
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) uses the camera to scan barcodes and QR codes.</string>
Android android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<!-- Only for vibrate on scan: -->
<uses-permission android:name="android.permission.VIBRATE" />
To bundle the ML Kit model on Android instead of downloading it through Google Play
services on first use, add to android/gradle.properties:
VisionCamera_enableCodeScanner=true
Both components ask for the camera permission themselves when they mount.
QRGenScanner (vision-camera)
import { useIsFocused } from '@react-navigation/native';
import { QRGenScanner } from 'qrgen-react-native';
export function ScanScreen() {
const isFocused = useIsFocused();
return (
<QRGenScanner
style={{ flex: 1 }}
isActive={isFocused}
symbologies={['qr', 'ean13', 'code128']}
mode="single"
onScan={(barcodes) => console.log(barcodes[0].symbology, barcodes[0].data)}
onError={(e) => console.warn(e.code, e.message)}
/>
);
}
The component:
- resolves SPEC ids, aliases and groups (
QRCode,ean-13,retail,2d, …) and maps them to vision-camera code types, dropping the ones the platform cannot read (seesupportedSymbologies()); - refines results so they match the other QRGen SDKs: on iOS, UPC-A arrives as EAN-13 with
a leading
0and is reported as a 12 digitupca, EAN-13 starting with978/979is reported asisbn, and a 14 digit ITF asitf14, whenever those ids were requested; - stops the camera while the app is in the background; pass
isActive={false}(for example fromuseIsFocused()) when the screen is not visible; - draws a rounded corner-bracket viewfinder (or a scan line for linear-only sets), highlight
frames around detected codes and a success toast pill, in the QRGen accent
#2EC1CE.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
symbologies |
string[] |
['all'] |
SPEC ids, aliases or groups |
mode |
'single' | 'continuous' | 'batch' |
'continuous' |
single stops the camera after the first scan (call ref.start() to scan again); batch tracks many codes |
duplicateFilter |
number (ms) |
1000 |
Ignore the same symbology + data inside this window. 0 reports every frame, -1 once per session |
beep |
boolean | () => void |
true |
Sound on scan, see Beep |
vibrate |
boolean |
true |
Vibration.vibrate(50) on scan |
camera |
'back' | 'front' | deviceId |
'back' |
Unknown device ids fall back to the back camera |
torch |
boolean |
false |
Flashlight (ignored with an unsupported error when the camera has none) |
viewfinder |
'frame' | 'line' | 'none' |
'frame' ('line' for linear-only sets) |
Overlay style |
scanArea |
{ x, y, width, height } (0..1 of the frame) |
full frame | Codes whose center is outside are ignored; the viewfinder is drawn there |
maxResults |
number |
1 (20 in batch) |
Codes per frame; the ones closest to the center win |
spatialTracking |
boolean |
false |
Batch mode: separate equal-payload observations by position |
trackMaxJump |
number |
3 |
Nonnegative finite movement gate relative to detection size |
trackMaxAssociationWork |
positive safe integer | 250000 |
Bound spatial association work; reject the complete frame above the limit |
isActive |
boolean |
true |
Run the camera |
onScan |
(barcodes: Barcode[]) => void |
New codes (after the duplicate filter) | |
onTrack |
(tracked: TrackedBarcode[]) => void |
Batch mode: every tracked code, whenever the set changes | |
onError |
(error: { code, message }) => void |
SPEC error codes | |
onReady |
() => void |
Camera initialized | |
accentColor |
string |
'#2EC1CE' |
Viewfinder, highlight and toast color |
showToast |
boolean |
true |
Success toast pill |
showHighlights |
boolean |
true |
Highlight frames around detected codes |
fallback |
ReactNode |
built-in message | Shown when the camera cannot run |
children |
ReactNode |
Rendered on top (buttons, hints) | |
style |
ViewStyle |
Container style (give it a size, for example flex: 1) |
Optional native spatial observations
Opt into position-based batch observations with spatialTracking. The default
continues to aggregate equal symbology and payload values. onScan still applies
the payload duplicate window; two visible identical labels can produce two
onTrack entries and one onScan entry.
<QRGenScanner
style={{ flex: 1 }}
mode="batch"
spatialTracking
onTrack={(observations) => console.log(observations)}
/>
Within each exact symbology/payload group, matching maximizes eligible one-to-one
associations inside the movement gate, then minimizes total center movement.
The gate is trackMaxJump times the incoming detection’s largest extent, with
a minimum extent of one pixel. Incoming detection order does not add an extra
observation when a complete eligible match exists. Spatial IDs are opaque and
fresh after expiry or reset within a session. Replacing the tracking policy uses
a new ID namespace. count is the number of decoded frame observations; tracks
do not establish confirmed product quantities or physical identity through occlusion.
The three native props are reactive. An equal effective policy keeps tracks.
A successful policy replacement clears tracks and preserves duplicate history.
A rejected proposal reports through onError and retains the accepted settings
and histories. Actual permission loss, device removal or isActive={false} still gates that camera;
view-axis or preview-orientation changes still establish coordinate boundaries.
Camera-source and coordinate boundaries clear spatial observations while keeping
duplicate history. Clearing a nonempty set while remaining in batch produces one
current empty tracking notification. Default payload mode keeps its existing
camera behavior.
Spatial batch frames require finite nonzero geometry and consistent positive integer
upright frame dimensions. Blank and ROI-excluded frames also carry full-frame
dimensions. Invalid geometry, clock or work-budget updates retain both histories
and IDs and report through onError.
The work estimate sums n*n*(m+n) per exact payload group with n current
detections and m > 0 surviving tracks, including out-of-gate prior columns.
New-only groups cost zero. Exceeding the budget rejects the complete frame before
assignment matrices or state changes; it does not truncate the result. The budget
bounds association work, not frame time or memory. Larger limits need workload testing.
These options belong to QRGenScanner, ScanSession and BarcodeTracker;
they are not embed URL or QRGenWebScanner options. Pure users can construct
new BarcodeTracker({ spatial: true, maxJump: 3, maxAssociationWork: 250000 })
or new ScanSession({ mode: 'batch', spatialTracking: true }) and pass an optional
source size to update(barcodes, now?, frameSize?) / process(...) for blank
resize detection. Direct same-size source switches must call reset() on the
tracker or resetSpatial() on the session. resetSpatial() preserves duplicate
history and stopped state; full session reset() clears both histories and resumes.
Camera callback ownership is checked in JavaScript. Physical camera alignment, dense-scene latency, long phone sessions and comparison with Scandit still require device testing.
Ref
const ref = useRef<QRGenScannerHandle>(null);
<QRGenScanner ref={ref} mode="single" onScan={...} />;
ref.current?.start(); // scan again after a single-mode result
| Method | Description |
|---|---|
start() / resume() |
Run again after stop(), pause() or a single-mode scan |
stop() |
Stop the camera |
pause() |
Keep the preview running but ignore codes |
reset() |
Clear duplicate history and tracked codes, and resume |
setTorch(on) |
Flashlight on or off (until the torch prop changes) |
supportedSymbologies() |
Ids this back end reads on this device |
Beep
React Native cannot play a sound without a library, and QRGen does not force one on
you. beep (default true) plays whatever you register once with setBeepHandler,
and is silent until you do. You can also pass a function as the beep prop.
import { createAudioPlayer } from 'expo-audio';
import { setBeepHandler } from 'qrgen-react-native';
const player = createAudioPlayer(require('./assets/beep.mp3'));
setBeepHandler(() => {
player.seekTo(0);
player.play();
});
react-native-sound, expo-av or any other player works the same way.
QRGenWebScanner (WebView, zero native setup)
Loads the hosted page https://rockyljewell.github.io/QR-GEN/embed/ with the options in
the query string (SPEC section 5) and forwards its events. It reads every QRGen
symbology (the page runs the QRGen WebAssembly engine), beeps from the page and vibrates
natively.
import { useRef } from 'react';
import { QRGenWebScanner, type QRGenWebScannerHandle } from 'qrgen-react-native';
export function WebScanScreen() {
const ref = useRef<QRGenWebScannerHandle>(null);
return (
<QRGenWebScanner
ref={ref}
style={{ flex: 1 }}
symbologies={['qr', 'pdf417']}
mode="continuous"
onReady={() => console.log('ready')}
onScan={(barcodes) => console.log(barcodes)}
onError={(e) => console.warn(e.code, e.message)}
/>
);
}
It accepts the same scanner options as QRGenScanner (symbologies, mode,
duplicateFilter, beep, vibrate, camera, torch, viewfinder, scanArea,
maxResults) plus:
| Prop | Type | Default | Description |
|---|---|---|---|
embedUrl |
string |
hosted page | Self-hosted copy of the embed page (must be https) |
isActive |
boolean |
true |
Sends start / stop to the page; the page is also stopped while the app is in the background |
onScan, onTrack, onError, onReady |
Same as above, from the page’s scan, track, error and ready messages |
||
fallback |
ReactNode |
built-in message | Shown when the WebView cannot run |
webViewProps |
object |
Extra props for the underlying <WebView> |
Ref methods: start(), stop(), pause(), resume(), setTorch(on), sendCommand(command),
reload(). Ref commands use the WebView’s JavaScript bridge,
and camera-dependent commands wait for the page’s actual ready message.
start/stop can reach a verified command bridge before camera-ready, including
with autostart=0 or after an initial camera error. Transport acknowledgement
does not call onReady. Private transport messages stay internal. Load and page
message handling also runs when advanced
webViewProps callbacks are supplied; those callbacks are invoked afterward.
The WebView is configured with mediaPlaybackRequiresUserAction={false},
allowsInlineMediaPlayback, and mediaCapturePermissionGrantType="grant" (iOS 15+, so the
page does not ask a second time). On Android the component requests the CAMERA runtime
permission before it mounts the WebView; react-native-webview then grants the page’s
getUserMedia request. On iOS the page’s camera request uses your app’s
NSCameraUsageDescription.
Results
onScan receives SPEC section 2 objects:
interface Barcode {
data: string; // decoded text
symbology: SymbologyId; // 'qr', 'ean13', ...
symbologyName: string; // 'QR Code'
rawBytes: string; // base64, '' when the engine does not expose bytes (vision-camera)
contentType: 'text' | 'binary' | 'gs1' | 'iso15434' | 'mixed' | 'unknown-eci';
isGS1: boolean;
location: { topLeft; topRight; bottomRight; bottomLeft }; // pixels of the upright frame
frameSize: { width: number; height: number }; // the upright frame
orientation: number;
ecLevel: string;
symbologyIdentifier: string;
timestamp: number; // ms since epoch
}
With QRGenScanner, location and frameSize describe the camera frame as it appears
upright on screen (the sensor’s landscape size is swapped for portrait views);
contentType is text except for GS1 DataBar, which is reported in HRI form
((01)...). Batch mode adds TrackedBarcode fields: id (derived from symbology
and data by default, with collision suffixes when needed; an opaque fresh observation ID
with spatialTracking), firstSeen, lastSeen and count.
IDs are opaque and unique among active tracks; they stay stable while a track remains visible.
Codes are dropped after more than 500 ms unseen, before matching the next frame. A returning
code starts fresh timestamps and count. Default payload mode can reuse a deterministic base ID.
Native duplicate windows and tracking expiry use elapsed monotonic time, so changing the
system clock does not extend or shorten them. timestamp, firstSeen and lastSeen remain
milliseconds since epoch. Pure helpers accept an optional now for an injected clock;
switching between injected and default clocks preserves retained timestamp age. Environments
without performance.now() retain the wall-clock fallback.
In native batch mode, onScan runs before onTrack. If onScan resets the scanner,
synchronously unmounts it, commits a different mode or spatial policy/source/coordinate boundary,
or processes a newer tracking update, the obsolete
track snapshot is discarded. An unchanged session, a cosmetic prop change, or an idempotent
start() / resume() keeps the current track callback and duplicate history.
A no-change empty frame also preserves the still-current notification; an empty frame
that expires tracks delivers the replacement list.
Errors are { code, message } with code one of camera-permission-denied,
camera-not-found, camera-in-use, insecure-context, engine-load-failed,
unsupported, unknown.
Parsing the content
The core JavaScript SDK exposes pure parsers that run in React Native (no DOM, no
WebAssembly). Install qrgen-sdk next to this package if you need them:
import { parseContent, parseGS1, parseAAMVA } from 'qrgen-sdk/parsers';
const parsed = parseContent(barcode.data); // url, wifi, contact, payment, product, gs1, aamva, ...
const gs1 = parseGS1('(01)09501101530003(17)250101(10)ABC123');
const license = parseAAMVA(pdf417Barcode.data); // driver license / ID card back side
Utilities
Pure helpers, also usable in Node and tests:
resolveSymbologies(input),resolveSymbology(id),symbologyName(id),SYMBOLOGIES,SYMBOLOGY_GROUPStoVisionCameraCodeTypes(ids, { os, version }),fromVisionCameraCode(type, value, requested),visionCameraSupportedSymbologies(platform)DuplicateFilter,BarcodeTracker,trackingId,ScanSessionbuildEmbedUrl(url, options),parseEmbedMessage(json),buildCommandScript(command)frameToViewTransform,uprightFrameSize,quadrilateralToView(maplocationto screen points)isVisionCameraAvailable(),isWebViewAvailable(): pick a back end at runtime
QRGenScanner loads vision-camera lazily, so importing this package in Expo Go (where
the native module is missing) does not crash. QRGenScanner then reports
engine-load-failed and you can fall back to QRGenWebScanner:
{isVisionCameraAvailable() ? <QRGenScanner {...props} /> : <QRGenWebScanner {...props} />}
Example
example/App.tsx is a small screen with a mode switch, a torch toggle,
a back end switch and a result list.
Status
- Tested:
npm testruns Jest unit tests (ts-jest, Node) for symbology resolution (ids, aliases, groups), the vision-camera mapping in both directions per platform, the duplicate filter, the batch tracker, the scan session state machine (single, continuous, batch), the frame-to-view math (aspect-fill crop, mirroring), code conversion, the embed URL builder, embed message parsing and commands, error mapping, and an import test of the entry point against a mockedreact-native.npx tsc --noEmittype-checks all sources, tests and the example against react-native 0.87, react-native-vision-camera 4.7.3 and react-native-webview 14. - Not tested yet: the components have not been run on a device or simulator. The
coordinate handling follows the vision-camera 4.7 native sources (iOS multiplies
preview-oriented normalized coordinates by the landscape sensor size; Android reports
ML Kit coordinates in the rotated image), and highlight placement, especially with the
front camera, still needs checking on hardware.
QRGenWebScannerdepends on the hosted embed page implementing SPEC section 5.
License
Apache-2.0, see LICENSE and NOTICE.
Created by Rockyljewell.