Almost every client app we pick up eventually grows a camera feature. Scan a barcode at a job site. Photograph a damaged part. Capture an ID or an invoice and pull the numbers off it. On paper it is one screen. In practice camera work is one of the few areas where a React Native team can lose three weeks, because it sits on top of two very different native camera stacks, a real-time pixel pipeline, and a permissions story that store reviewers read carefully.
This post is the decision tree and the plumbing we use on client projects.
Step 1: pick the right library, once
There are two serious options in 2026, and the choice is mostly about whether you need to look at frames or just capture them.
expo-camera is the right default when the camera is an input device: take a photo, record a clip, scan a barcode. It ships with Expo, it is config-plugin friendly, and its built-in barcodeScannerSettings covers the overwhelming majority of scanning requirements without a single line of native code.
import { CameraView, useCameraPermissions } from 'expo-camera';
export function ScanScreen({ onCode }: { onCode: (v: string) => void }) {
const [permission, requestPermission] = useCameraPermissions();
if (!permission) return null;
if (!permission.granted) {
return <PermissionPrompt onPress={requestPermission} />;
}
return (
<CameraView
style={{ flex: 1 }}
facing="back"
barcodeScannerSettings={{ barcodeTypes: ['qr', 'ean13', 'code128'] }}
onBarcodeScanned={({ data }) => onCode(data)}
/>
);
}
react-native-vision-camera is the right choice when you need the frames themselves: a custom ML model, live document edge detection, OCR overlays, pose or face tracking, custom exposure and focus control, or multi-camera devices. It exposes a frame processor — a JS function that runs on every frame on a dedicated worklet thread — and that is the capability expo-camera does not give you.
Our rule of thumb: if the product requirement contains the phrase "in real time", you want VisionCamera. If it contains "take a picture of", you want expo-camera. Do not adopt both in the same app to hedge; two camera sessions competing for the same hardware is a debugging experience nobody enjoys.
Step 2: understand where frame processors actually run
A frame processor is not a React callback. It is a worklet, compiled by Reanimated's Babel plugin and executed on a separate native thread with its own JS runtime. It gets a Frame object that is a short-lived handle to a native buffer — valid only for the duration of that call.
import { Camera, useCameraDevice, useFrameProcessor } from 'react-native-vision-camera';
import { useSharedValue } from 'react-native-worklets-core';
export function DocumentCamera() {
const device = useCameraDevice('back');
const brightness = useSharedValue(0);
const frameProcessor = useFrameProcessor((frame) => {
'worklet';
// runs on the frame processor thread, ~30-60x per second
const result = detectDocumentEdges(frame); // a native frame processor plugin
brightness.value = result.averageLuma;
}, [brightness]);
if (!device) return null;
return (
<Camera
style={{ flex: 1 }}
device={device}
isActive
frameProcessor={frameProcessor}
/>
);
}
Three things bite teams here, every time:
- You cannot call normal JS from a worklet. No
fetch, no Redux dispatch, nosetState. Hand results back with a shared value, or marshal to the JS thread explicitly (runOnJS/Worklets.createRunOnJS) at a throttled rate — not per frame. - Do not retain the frame. Copying a 4K buffer into JS on every frame is the single most common cause of "the camera screen gets hot and then crashes". Do the heavy work in a native frame processor plugin (Swift/Kotlin) and return a small result object.
- Frames are dropped, and that is correct. If your processor takes 40 ms, you get ~25 fps of analysis on a 60 fps stream. Budget explicitly; do not assume every frame is seen.
Step 3: use the platform's scanners before you use a model
Before anyone reaches for a custom model, check whether the OS already does it. On iOS, VNDocumentCameraViewController gives you a full multi-page document scanner with edge detection and perspective correction for free, and VNRecognizeTextRequest does high-quality on-device OCR. On Android, ML Kit's document scanner and text recognition modules do the same, and Play Services can deliver the models on demand so your APK does not carry them.
In React Native that means one thin native module (or an existing wrapper) that returns file URIs and recognised text, instead of a frame-by-frame pipeline you own. It is dramatically less code, it runs offline, and it keeps images off your servers — which is the answer you want when a client's security team asks where the ID photos go.
Reach for a custom model (TFLite via a VisionCamera plugin, or Core ML / NNAPI directly) only when you need something the platform genuinely does not offer: a domain-specific classifier, a damage-detection model, a barcode symbology nobody supports.
Step 4: capture settings, orientation, and file size
The boring layer is where quality complaints come from.
- Resize before upload. A modern phone sensor happily produces a 6 MB HEIC. If your backend needs a 1600 px long edge, downscale on device (
expo-image-manipulatoror VisionCamera'sphotooptions) before the upload ever starts. Field users on bad connections will notice more than any other optimisation you make. - Respect EXIF orientation. Your preview looks right and the uploaded image is sideways because you stripped EXIF. Either preserve it or physically rotate the pixels — pick one and enforce it in a helper everyone uses.
- Write to cache, not documents. Captured originals belong in the cache directory so the OS can reclaim them; only move a file to documents once it is part of durable user data. This also keeps your iCloud backup size sane.
- Handle the failure states in the UI. Denied permission, camera in use by another app, no torch on this device, storage full. All four happen in production, and all four are one-line guards.
Step 5: permissions and the store review story
Camera features are a common rejection cause, and the fixes are cheap:
NSCameraUsageDescriptionandNSMicrophoneUsageDescriptionmust describe the feature, not the permission. "To scan barcodes on your work orders" passes; "This app uses the camera" invites a reply.- If you only take photos, do not request microphone access. On VisionCamera that means leaving
audiooff; onexpo-camerait means configuring the plugin without the microphone permission. Every unused permission is a question you have to answer twice — once at review, once in the privacy label. - On Android, declare
android.permission.CAMERAand checkhasSystemFeaturerather than assuming; also remember that saving to shared storage has followed scoped-storage rules for years now. - If images leave the device, say so in the privacy nutrition label and Data Safety form, and match it to what your code actually uploads. Reviewers do compare.
Step 6: test it on hardware, in the dark
Simulators do not have cameras worth trusting. Our minimum matrix before a camera feature ships:
- One low-end Android device (the scan loop is a frame-rate test, and mid-range Androids are where it fails).
- One recent iPhone with a multi-camera system, to confirm you get the lens you asked for rather than the ultra-wide.
- Poor lighting, plus a torch-on pass.
- Backgrounding mid-capture, and an incoming call during a recording.
- Orientation changes if the screen is not locked to portrait.
Automate what you can: a Maestro flow that opens the scan screen, grants permission, and asserts the result screen renders will catch the permission regressions that otherwise reach users.
A short checklist
- Chosen library matches the requirement (capture vs. frame access), and only one camera library is installed.
- Frame processors do their heavy work natively and marshal small, throttled results to JS.
- Platform scanners/OCR evaluated before any custom model.
- Images downscaled and EXIF handled before upload; originals in cache.
- Permission strings describe the feature; unused permissions removed.
- Privacy labels match what actually leaves the device.
- Tested on a low-end Android, in bad light, with interruptions.
Where we fit
Camera and scanning features are the kind of work where an experienced pair of eyes saves more time than it costs — most of the cost is in knowing which of the above steps you can skip. Our React Native consultants regularly come in to scope a scanning or capture feature, build the native frame processor plugin, and hand the team a pipeline they can maintain. If you have a camera feature on the roadmap, get in touch and we will walk through the tradeoffs with you.