@cloudstrytech/react-expo-camera
v0.1.0
Published
A camera component for React Native / Expo apps on Android. Live preview, permissions, zoom, capture shapes and automatic image optimisation.
Readme
@cloudstrytech/react-expo-camera
A camera component for React Native / Expo apps on Android.
One package, one job: a camera. There is no second component and no subpath
import — install it, import CloudstryCamera, done.
Status: 0.1.0.
CloudstryCamerais working: live preview, permissions, capture, front/back switch, close button, pinch and slider zoom, five capture shapes, and automatic image optimisation. Checked on 3 real Android devices.
Install
npm install @cloudstrytech/react-expo-camera
npx expo install expo-camera expo-image-manipulatorBoth are peer dependencies — declared, not bundled. If the library bundled
its own copy, an app that already uses expo-camera would end up with two
copies and two native module registrations. Peer dependencies force a single
shared copy.
The library itself has zero runtime dependencies and needs no Babel plugin, no root wrapper, and no Metro config.
Quick start
import { View } from "react-native";
import { CloudstryCamera } from "@cloudstrytech/react-expo-camera";
export default function Screen() {
return (
<View style={{ flex: 1 }}>
<CloudstryCamera onCapture={(photo) => console.log(photo.uri)} />
</View>
);
}That is all. The component asks for camera permission itself.
Give it a parent with flex: 1. The camera fills its parent — with no
height, nothing shows.
You can also import just this component:
import { CloudstryCamera } from "@cloudstrytech/react-expo-camera";What you get back
{
uri: "file:///data/user/0/.../photo.jpg",
width: 1499,
height: 2665
}The component stops here. It does not upload and does not save to the gallery — one URI lets your app do either.
The file lives in the app cache. Use it soon after capture; Android can clear cache when storage runs low.
Props
All optional.
Camera
| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| facing | "back" | "front" | "back" | Which camera to use |
| showFlipButton | boolean | true | Show the front/back switch |
| style | ViewStyle | — | Style for the camera container |
Photo shape
| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| ratio | "16:9" | "1:1" | "4:5" | "2:3" | "9:16" | "9:16" | Shape of the photo |
| allowedRatios | array of the above | all five | Which shapes the user can pick |
| showRatioPicker | boolean | true | Show the shape buttons |
| onRatioChange | (ratio) => void | — | Fires when the user picks a shape |
The preview is framed to the shape and the saved file is centre-cropped to match, so what the user sees is what they get.
File size
| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| quality | number 0–1 | 0.85 | JPEG compression |
| maxPixels | number | 4000000 | Pixel ceiling for the saved photo |
Defaults keep a photo near 1–1.5 MB. A raw sensor capture is 7–8 MB. Lower
maxPixels for smaller files. Photos are never scaled up.
Zoom
| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| zoom | number 0–1 | 0 | Starting zoom |
| enableZoom | boolean | true | Turn zoom off completely |
| showZoomSlider | boolean | true | Show the slider. Pinch still works |
| onZoomChange | (zoom) => void | — | Fires when the user zooms |
Callbacks
| Prop | Type | What it does |
| --- | --- | --- |
| onCapture | (photo) => void | A photo was taken |
| onCancel | () => void | User tapped the close button |
| onError | (error) => void | Something failed |
The close button appears only if you pass onCancel. The library does not
close itself — it cannot know how it was mounted. onCancel is the signal;
your app unmounts the component, and that unmount releases the camera. It also
adds a Cancel option to the permission screen, so a user who denies permission
is never stuck.
Recipes
Fixed shape, no choice
<CloudstryCamera ratio="4:5" showRatioPicker={false} />Only some shapes — one entry hides the picker, since one button is not a choice.
<CloudstryCamera allowedRatios={["1:1", "9:16"]} />Plain shutter only
<CloudstryCamera enableZoom={false} showFlipButton={false} />Many photos — the camera never closes itself, so just keep it mounted.
const [photos, setPhotos] = useState([]);
<CloudstryCamera onCapture={(p) => setPhotos((list) => [...list, p])} />Save to the gallery — install expo-media-library in your app.
import * as MediaLibrary from "expo-media-library";
const [perm, requestPerm] = MediaLibrary.usePermissions({ writeOnly: true });
<CloudstryCamera
onCapture={async (photo) => {
if (!perm?.granted) await requestPerm();
await MediaLibrary.saveToLibraryAsync(photo.uri);
}}
/>Upload
onCapture={async (photo) => {
const blob = await fetch(photo.uri).then((r) => r.blob());
await fetch(presignedUrl, { method: "PUT", body: blob });
}}Permissions
Camera permission is handled for you. You write no permission code.
| What happened | What the user sees | | --- | --- | | Allowed | The camera | | Denied | A message with "Try again" | | Denied forever | A message with a button to open system settings |
Pass onCancel for a Cancel option on this screen too.
Gallery permission is separate — ask for it yourself if you save photos.
Not included
Save to gallery · upload · rotate · draggable crop · video · barcodes · filters · iOS
The component returns a URI and stops. Storage and upload are your app's decision, and one URI serves every option.
Types
import type {
CloudstryCameraProps,
CapturedPhoto,
CameraFacing,
CaptureRatio,
} from "@cloudstrytech/react-expo-camera";Requirements
| Item | Minimum | | --- | --- | | Platform | Android only | | React | 18 | | React Native | 0.73 | | expo-camera | 16 | | expo-image-manipulator | 13 |
Built and tested against Expo SDK 54.
Troubleshooting
| Problem | Fix |
| --- | --- |
| Nothing shows | Parent needs flex: 1 |
| Camera stays open after a photo | It never closes itself — set your own state in onCapture |
| No close button | Pass onCancel |
| File missing later | Upload or save right after capture; cache is not permanent |
| ratio ignored | allowedRatios excludes it — the allowed list wins |
| Files too big | Lower maxPixels |
License
MIT
