skip to content

In an Expo app, how do you take a photo with expo-camera's CameraView, and what must the code wait for before capturing?

level: juniorimportance: must knowfreq 50%

answer

  1. a ref to the camera component
  2. permission first, preview second
  3. onCameraReady before capturing
  4. takePictureAsync returns a temporary uri
  5. one active preview at a time

basics

~20 s

Render CameraView once camera permission is granted, keep a ref, wait for onCameraReady, then call ref.current.takePictureAsync(options). It resolves with a uri to a temporary cache file, so copy it somewhere permanent before relying on it.

solid answer

~40 s

`CameraView` from `expo-camera` renders the live preview; `facing` defaults to `'back'` and `flash` to `'off'`. I gate it on `useCameraPermissions`, hold a ref, and enable the capture button only after `onCameraReady` fires, because the docs say to wait for it before calling `takePictureAsync`. The call accepts options such as `quality` (0 to 1, default 1), `base64`, `exif` and `skipProcessing`, and resolves with `uri`, `width` and `height`. The `uri` points to a temporary file in the app's cache, so a receipt must be copied to permanent storage. Only one camera preview can be active at a time, so the `CameraView` should unmount when its screen loses focus. On iOS the `active` prop can also stop the session without unmounting.

code

tsx · 27 lines
tsx
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useRef, useState } from 'react';
import { Button, View } from 'react-native';

export function ReceiptCamera({ onCaptured }: { onCaptured: (uri: string) => void }) {
  const [permission, requestPermission] = useCameraPermissions();
  const camera = useRef<CameraView>(null);
  const [ready, setReady] = useState(false);

  if (!permission) return null;
  if (!permission.granted) {
    return <Button title="Allow camera to scan receipts" onPress={requestPermission} />;
  }

  const capture = async () => {
    const photo = await camera.current?.takePictureAsync({ quality: 0.7 });
    // photo.uri is a temporary cache file: copy it before saving the receipt.
    if (photo) onCaptured(photo.uri);
  };

  return (
    <View style={{ flex: 1 }}>
      <CameraView ref={camera} style={{ flex: 1 }} facing="back" onCameraReady={() => setReady(true)} />
      <Button title="Capture receipt" disabled={!ready} onPress={capture} />
    </View>
  );
}

go deeper

for a junior

Recall the sequence: permission, a ref to CameraView, wait for onCameraReady, then takePictureAsync, and that the returned uri is temporary.

for a middle

Explain the capture options, quality, base64, exif and skipProcessing, and why only one preview can be active at a time.

for a senior

Diagnose black previews and failed captures across navigators and platforms, and decide which native options, such as barcode scanning or audio permission, a receipt app should drop.

for a principal

Weigh an in-app camera against the system camera UI for a capture flow, considering control, permissions, app size and maintenance.

## The component and its defaults `expo-camera` exports **`CameraView`**, a native view that shows the live camera preview and exposes capture methods through a **ref**. The props that matter for a receipt scanner: | prop | default | purpose | |---|---|---| | `facing` | `'back'` | which camera, `'front'` or `'back'` | | `flash` | `'off'` | `'on'`, `'off'` or `'auto'` for the still photo | | `enableTorch` | `false` | keeps the light on, useful for dim receipts | | `mode` | `'picture'` | `'picture'` or `'video'` | | `zoom` | `0` | 0 to 1 of the device's maximum zoom | | `active` | `true` | iOS only: stop the session without unmounting | | `onCameraReady` | none | fires when the preview has started | | `onMountError` | none | fires when the preview could not start | ## The capture sequence 1. **Permission**: read and request camera access with `useCameraPermissions` from `expo-camera`; render the preview only once it is granted. 2. **Ref**: keep a ref to the `CameraView` instance; capture methods live on it. 3. **Ready**: wait for **`onCameraReady`**. The source documents this explicitly for `takePictureAsync`: calling earlier risks a failed or empty capture. 4. **Capture**: `await ref.current.takePictureAsync(options)`. 5. **Persist**: move the result out of the cache before relying on it. ## What takePictureAsync takes and returns Options include: - **`quality`**: 0 to 1, default **1**; lower values shrink the file, which suits receipts that only need to be legible. - **`base64`** and **`exif`**: also return the JPEG as Base64 or the EXIF tags. - **`skipProcessing`**: skips orientation correction and the processing pipeline, so it is faster but can leave the photo rotated, and `quality` is ignored. - **`shutterSound`**: default `true`; set `false` to silence it. The promise resolves with **`uri`**, **`width`**, **`height`** and the optional extras. On native platforms the **`uri` is a temporary file in the app's cache directory**; the docs recommend copying it with the file-system package to keep it. Two timing details from the source: - Calling `takePictureAsync` **while the preview is paused** throws on Android and returns the last on-screen frame on iOS. - Passing `onPictureSaved` makes the promise resolve immediately, with the picture delivered to that callback once saved. ## One preview at a time The docs warn that **only one camera preview can be active at any given time**. In a tabbed or stacked app, a camera screen that stays mounted in the background can leave the next camera screen black. The fix is to render `CameraView` only while its screen is focused. On iOS, the `active` prop can additionally stop the session while the view stays mounted. ## Reading a code printed on the receipt Many receipts carry a QR code. `CameraView` can read it during the preview: - pass **`barcodeScannerSettings={{ barcodeTypes: ['qr'] }}`** to limit the types scanned; - handle **`onBarcodeScanned`**, which receives the code's `type` and `data`; - guard against repeats: the component drops an identical result only if it arrives within 500 ms of the previous one, so the same code keeps firing while it stays in frame. Set a flag, or stop passing `onBarcodeScanned`, after the first read. Scanning relies on the barcode support that the `barcodeScannerEnabled` plugin option controls. ## Native configuration `expo-camera` has a config plugin whose options become part of the native build: - **`recordAudioAndroid`**, default `true`, adds Android's audio recording permission for video; a photo-only receipt scanner can set it to `false`. - **`barcodeScannerEnabled`**, default `true`, includes barcode scanning support; setting it to `false` reduces app size when the app never reads codes. - The iOS camera and microphone prompt texts are set through the same plugin. Changing any of them requires a new native build. ## Common mistakes - Calling `takePictureAsync` from a `useEffect` on mount, before `onCameraReady`. - Storing the returned `uri` in the database as if it were permanent. - Leaving a camera screen mounted under a navigator while another camera screen opens.

  • Why can a second camera screen show a black preview in an Expo app with tabs?
    Only one camera preview can be active at a time. If the first screen's `CameraView` stays mounted in an inactive tab, it can hold the camera. Render the `CameraView` only while its screen is focused, and on iOS the `active` prop can stop the session without unmounting.
  • When would you set skipProcessing: true in takePictureAsync?
    When capture latency matters more than orientation, for example for a quick throwaway frame. It skips the processing pipeline, so the photo can come back rotated on some devices and `quality` is ignored. For receipts that must be read later, keep processing on.

saying these in an interview costs you the question

  • takePictureAsync can be called as soon as the component mounts.
  • The uri returned by takePictureAsync is a permanent file.
  • Several CameraView previews can run at once on different screens.
  • CameraView's active prop works the same on Android and iOS.
  • skipProcessing only makes capture faster with no side effects.