This episode covers media: the camera with react-native-vision-camera, image picker, image resize and compression, permission management with react-native-permissions, usage descriptions in Info.plist, and UX when the user denies permission.

The camera and gallery are the features that most often trigger permission questions and the first crash in a mobile app. One wrong declaration in Info.plist or AndroidManifest.xml, and the app closes immediately when the user taps the camera button.
Episode 12 covers media end to end: accessing the camera with react-native-vision-camera, choosing images with an image picker, resizing and compressing, then managing permissions with react-native-permissions — including usage descriptions in Info.plist and the user experience when permission is denied.
VisionCamera is a modern JSI-based camera library. In an Expo project, install it via npx expo install so the version matches the SDK:
npx expo install react-native-vision-cameraAdd the camera usage description in Info.plist for iOS and the permission declaration in AndroidManifest.xml for Android before running the app.
import { Camera, useCameraDevice } from "react-native-vision-camera";
import { Text, View } from "react-native";
export function KameraScreen() {
const device = useCameraDevice("back");
if (device == null) {
return <Text>Kamera tidak tersedia</Text>;
}
return (
<View style={{ flex: 1 }}>
<Camera
style={{ flex: 1 }}
device={device}
isActive={true}
photo={true}
/>
</View>
);
}useCameraDevice("back") selects the rear camera. The isActive property controls when the camera is on — turn it off when the screen isn't visible to save battery. photo={true} enables photo capture mode.
The takePhoto() result path from the camera ref — a temporary file — is ready for further processing: resize, compress, or upload to the server.
Not every flow needs the camera directly. To pick images already in the gallery, use an image picker:
npm install react-native-image-pickerimport { launchImageLibrary } from "react-native-image-picker";
function pilihGambar() {
launchImageLibrary({ mediaType: "photo" }, (res) => {
if (res.didCancel) return;
const uri = res.assets?.[0]?.uri;
prosesGambar(uri);
});
}Note res.didCancel — the user closing the picker without selecting is a normal occurrence, not an error. res.assets?.[0]?.uri uses optional chaining because the array can be empty.
Original images from modern cameras are a dozen megabytes. Uploading them raw burns data quota and slows down the UI. Resize and compress before uploading:
npm install react-native-image-resizerimport ImageResizer from "react-native-image-resizer";
async function siapkanUpload(uri) {
const hasil = await ImageResizer.createResizedImage(uri, 1200, 1200, "JPEG", 85);
return hasil.uri;
}createResizedImage(uri, 1200, 1200, "JPEG", 85) brings the resolution down to a maximum of 1200 pixels and JPEG quality of 85 percent — enough for screen display, light on the network.
Manage permissions across platforms with react-native-permissions — one API for iOS and Android:
npm install react-native-permissionsimport { check, request, PERMISSIONS, RESULTS } from "react-native-permissions";
import { Platform } from "react-native";
const kamera =
Platform.OS === "ios" ? PERMISSIONS.IOS.CAMERA : PERMISSIONS.ANDROID.CAMERA;
async function pastikanIzin() {
const status = await check(kamera);
if (status === RESULTS.DENIED) {
return request(kamera);
}
return status;
}Platform.OS === "ios" selects the correct permission constant per platform. The right sequence: check first, request only if the status is DENIED, and don't request again once it's BLOCKED — requesting again on iOS when already blocked silently returns without a dialog.
iOS requires a usage description for every permission used. Without it, the app crashes when the permission is requested:
NSCameraUsageDescription: Aplikasi memakai kamera untuk memotret foto profil.
NSPhotoLibraryUsageDescription: Aplikasi memakai galeri untuk memilih gambar.This description text appears in the iOS permission dialog — write something clear and honest about the purpose of the feature.
Users have the right to deny permission. When the status is BLOCKED, the dialog can no longer be shown — the only path is system settings. Handle it by showing an explanation and a button to open settings:
import { openSettings } from "react-native-permissions";
async function saatDiblokir() {
const izin = await pastikanIzin();
if (izin === RESULTS.BLOCKED) {
openSettings();
}
}openSettings() opens the app's settings screen in the system. The combination of a status check, an explanatory message, and a path to settings helps the user understand why the feature needs permission and how to enable it again.
The camera feature isn't everything. When permission is denied, show alternatives: manual input, upload from the gallery, or other features that don't need permission. A good app stays useful even when some permissions are denied.
Warning
Don't request many permissions at once at startup. The system and users will be suspicious, and denial rates rise dramatically. Request permission in the context of the relevant feature and explain its benefit before the dialog appears.
Episode 12 mastered media: the camera with VisionCamera, image selection and processing, cross-platform permissions with react-native-permissions, correct usage descriptions, and the user experience when permission is denied.
Key takeaways:
isActive when the screen isn't visible to save battery.DENIED, don't request again when BLOCKED.In the next episode, episode 13, we'll discuss secure storage and credentials: Keychain and Keystore for tokens, avoiding secrets in the bundle, per-build .env configuration, and habits that keep secrets from ever being committed.