Whether you're building a QR scanner, a document uploader, or a social app, the camera is a core feature — and expo-camera is the most-searched way to add it. The modern API is refreshingly small: one CameraView component and one useCameraPermissions hook. This tutorial covers preview, capture, flipping, and barcode scanning.
Install
npx expo install expo-camera
Add the config plugin in app.json so the OS shows a permission prompt with your copy:
{
"expo": {
"plugins": [
["expo-camera", { "cameraPermission": "Allow $(PRODUCT_NAME) to use the camera." }]
]
}
}
Request permission and show a preview
The useCameraPermissions hook returns the current status and a request function:
import { CameraView, useCameraPermissions } from "expo-camera"
import { Button, View } from "react-native"
export default function CameraScreen() {
const [permission, requestPermission] = useCameraPermissions()
if (!permission) return <View /> // still loading
if (!permission.granted) {
return <Button title="Grant camera access" onPress={requestPermission} />
}
return <CameraView style={{ flex: 1 }} facing="back" />
}
Take a photo
Attach a ref and call takePictureAsync():
import { CameraView } from "expo-camera"
import { useRef } from "react"
import { Button, View } from "react-native"
export default function Capture() {
const cameraRef = useRef<CameraView>(null)
async function snap() {
const photo = await cameraRef.current?.takePictureAsync()
console.log(photo?.uri) // file://... — display or upload this
}
return (
<View style={{ flex: 1 }}>
<CameraView ref={cameraRef} style={{ flex: 1 }} facing="back" />
<Button title="Take photo" onPress={snap} />
</View>
)
}
Flip between front and back cameras
facing is just a prop — toggle it in state:
import { useState } from "react"
const [facing, setFacing] = useState<"front" | "back">("back")
// <CameraView facing={facing} ... />
// <Button title="Flip" onPress={() => setFacing((f) => (f === "back" ? "front" : "back"))} />
Scan QR codes and barcodes
CameraView scans barcodes natively — no extra library:
<CameraView
style={{ flex: 1 }}
barcodeScannerSettings={{ barcodeTypes: ["qr", "ean13"] }}
onBarcodeScanned={({ data }) => {
console.log("Scanned:", data)
}}
/>
Debounce the handler in real apps, since it fires continuously while a code is in view.
Gotchas
- Call
takePictureAsync()only after permission is granted and the view has mounted. - On a physical device the preview is sharp; simulators show a placeholder.
- To save photos to the gallery, pair this with
expo-media-library; to upload, sendphoto.uriwithFormData.
Next steps
Need to let users pick an existing photo instead? See how to pick images in Expo with expo-image-picker. To display captured images efficiently, use expo-image. Full list: the top 10 Expo SDK modules.