React Native Intermediate: Navigation, Data and Device Features
Device Features and Permissions
In this lesson you will
- Follow the permission flow of checking, asking and handling a refusal
- Show a camera preview and read the phone's location with Expo libraries
- Pick a photo from the library and add haptic feedback
Introduction
A phone is not a small computer with a touch screen. It has a camera, a GPS receiver, a photo library, a vibration motor and more. Using them is what makes mobile apps feel special, and Expo wraps each one in a ready-made library that works on both iPhone and Android.
Because these features touch private things, phones make you ask the user first. This lesson teaches that permission flow and then three examples.
The permission flow
Every private feature follows the same pattern:
- Check whether you already have permission.
- If not, ask, and the phone shows its own system pop-up.
- If the user says no, handle it kindly and explain how to continue.
Rules that keep users happy, and keep your app in the stores:
- Ask when you need it, not at startup. Ask for the camera when the user taps "Take photo", not the moment the app opens.
- Explain why first. A short sentence on your screen, "We use the camera to scan the barcode", makes people more likely to agree.
- Never assume yes. Users can refuse, and on both phones they can turn a permission off later in Settings.
- After a refusal the phone may not let you ask again. The system pop-up may not appear a second time, so direct the user to the Settings app.
Quick quiz
When is the best moment to ask for camera permission?
Camera
Install the library and use the permission hook it provides:
Terminal
npx expo install expo-cameraReact (TSX)
import { CameraView, useCameraPermissions } from "expo-camera";
import { Pressable, StyleSheet, Text, View } from "react-native";
export default function Scanner() {
const [permission, requestPermission] = useCameraPermissions();
// 1. Still finding out whether we already have permission.
if (!permission) return <View />;
// 2. Not allowed yet: explain, then ask.
if (!permission.granted) {
return (
<View style={styles.center}>
<Text style={styles.message}>We use the camera to take photos of your notes.</Text>
<Pressable style={styles.button} onPress={requestPermission}>
<Text style={styles.label}>Allow camera</Text>
</Pressable>
{!permission.canAskAgain && <Text style={styles.hint}>Camera access is off. Turn it on in the Settings app.</Text>}
</View>
);
}
// 3. Allowed: show the live preview.
return <CameraView style={styles.camera} facing="back" />;
}
const styles = StyleSheet.create({
center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, gap: 12 },
message: { fontSize: 16, textAlign: "center" },
hint: { color: "#b91c1c", textAlign: "center" },
button: { backgroundColor: "#5b4bf5", paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8 },
label: { color: "white", fontWeight: "bold" },
camera: { flex: 1 },
});useCameraPermissions() returns the current permission object and a function to ask. The object is empty at first while the phone is checked. permission.granted says whether you may use the camera, and permission.canAskAgain says whether the system will still show its pop-up. CameraView is the live preview, and facing picks the back or front camera. Camera needs a real device or an emulator with a webcam set up, so test on your phone.
Location
Install expo-location. The functions are asynchronous, so you call them from an effect or an event handler:
React (TSX)
import * as Location from "expo-location";
import { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
export default function Where() {
const [coords, setCoords] = useState<Location.LocationObjectCoords | null>(null);
const [message, setMessage] = useState("Finding you...");
useEffect(() => {
async function find() {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
setMessage("Location is off, so we cannot show what is near you.");
return;
}
const position = await Location.getCurrentPositionAsync({});
setCoords(position.coords);
}
find();
}, []);
return (
<View style={styles.screen}>
{coords ? (
<Text style={styles.text}>
Latitude {coords.latitude.toFixed(4)}, longitude {coords.longitude.toFixed(4)}
</Text>
) : (
<Text style={styles.text}>{message}</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
text: { fontSize: 16, textAlign: "center" },
});requestForegroundPermissionsAsync asks for location while the app is open, which is enough for most apps. Asking for location in the background is a much bigger request, and stores review it closely, so avoid it unless you truly need it. getCurrentPositionAsync returns an object with coords, including latitude and longitude.
Photos from the library
expo-image-picker opens the phone's own photo picker:
React (TSX)
import * as ImagePicker from "expo-image-picker";
import { useState } from "react";
import { Image, Pressable, StyleSheet, Text, View } from "react-native";
export default function Avatar() {
const [uri, setUri] = useState<string | null>(null);
async function choose() {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: true,
aspect: [1, 1],
quality: 0.8,
});
if (!result.canceled) setUri(result.assets[0].uri);
}
return (
<View style={styles.screen}>
{uri && <Image source={{ uri }} style={styles.image} />}
<Pressable style={styles.button} onPress={choose}>
<Text style={styles.label}>Choose a photo</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: "center", justifyContent: "center", gap: 16 },
image: { width: 160, height: 160, borderRadius: 80 },
button: { backgroundColor: "#5b4bf5", paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8 },
label: { color: "white", fontWeight: "bold" },
});The result has a canceled flag, because the user can close the picker without choosing. When they choose, assets holds the picked files and each has a uri you can show in an Image. quality goes from 0 to 1 and trades size for detail.
A touch of feel: haptics
A tiny vibration when a button is pressed makes an app feel polished. expo-haptics provides it:
React (TSX)
import * as Haptics from "expo-haptics";
import { Pressable, Text } from "react-native";
export default function LikeButton() {
return (
<Pressable
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<Text style={{ fontSize: 32 }}>โฅ</Text>
</Pressable>
);
}Use it sparingly, for real actions such as a like or a completed task, not on every tap.
Telling the phone why you ask
On iPhone, an app must include a short text for each private feature that explains why it is needed, and that text is shown in the permission pop-up. In an Expo project you set these in the app configuration (app.json) through the library's config plugin, for example a cameraPermission message for expo-camera. The Expo docs for each library list the exact option names. Without them, a release build can crash when it asks, and the App Store can reject it. In Expo Go the libraries use default messages, so you only notice this when you build for real.
Quick quiz
What does permission.canAskAgain being false mean for the camera?
Try it yourself
Build a small "journal" screen: a button that asks for the camera or photo permission with an explanation, saves the chosen photo's uri with AsyncStorage from the last lesson, and shows it again when the app restarts. Test what happens when you deny the permission, then switch it back on in the phone's Settings.
Summary
- Phones guard private features, so follow the flow: check, ask when needed with an explanation, and handle a refusal without a dead end.
- Expo libraries wrap each feature:
expo-camerafor the camera,expo-locationfor position,expo-image-pickerfor photos andexpo-hapticsfor vibration. - On iPhone each private feature needs a reason text set through the library's config plugin in your app configuration.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading