Section 4 · Data and the Device
Location, Photos and Notifications
22 min3 min readCode playground
Lesson at a glance
Track a hike’s route with expo-location, attach trail photos with expo-image-picker, and schedule a reminder with expo-notifications, cleaning up subscriptions and filtering noisy GPS data.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Three device features make Trailhead more than a list: the route you walked, the photos you took, and a nudge at the end of the day to log the hike you forgot. They use three Expo modules with a lot in common: each needs a permission (asked the way the last lesson showed), each returns data you’ll store with the tools from two lessons ago, and each has one detail that bites if you skip it.
Tracking a route
getCurrentPositionAsync gives you one reading, which is enough to tag where a hike started. Tracking needs a stream: watchPositionAsync calls your function with each new position and returns a subscription that keeps the GPS running until you remove it.
import * as Location from 'expo-location';
import { useEffect, useState } from 'react';
export type Point = { latitude: number; longitude: number; accuracy: number | null; timestamp: number };
export function useRoute(active: boolean) {
const [points, setPoints] = useState<Point[]>([]);
useEffect(() => {
if (!active) return;
let subscription: Location.LocationSubscription | undefined;
let cancelled = false;
Location.watchPositionAsync(
{ accuracy: Location.Accuracy.High, distanceInterval: 10, timeInterval: 5000 },
({ coords, timestamp }) => {
setPoints((prev) => [...prev, { latitude: coords.latitude, longitude: coords.longitude, accuracy: coords.accuracy, timestamp }]);
},
).then((sub) => {
if (cancelled) sub.remove();
else subscription = sub;
});
return () => {
cancelled = true;
subscription?.remove();
};
}, [active]);
return points;
}The cancelled flag handles a subtle race: the subscription arrives asynchronously, so if the screen unmounts before the promise resolves, the cleanup has nothing to remove yet. The flag makes the late subscription remove itself.
Appending to an array in state on every reading is fine for a few thousand points, which covers a long day hike. For multi-day tracks, write points to SQLite in batches and keep only the recent ones in memory, so a single state update never copies tens of thousands of objects.
The options are a battery budget. Accuracy.High asks for a precision of about ten meters; Balanced is about a hundred meters and much cheaper. distanceInterval: 10 means “only call me after moving ten meters”, and timeInterval caps the frequency on Android. A hiking track needs high accuracy but not a point every second.
Tracking with the screen locked needs background location and a task registered with expo-task-manager, plus the extra permission and review scrutiny from the last lesson. Ship foreground tracking first; many hikers keep the screen on to see the map anyway.
Trail photos
For photos, expo-image-picker opens the system’s own photo library or camera UI, which is familiar to users and handles editing for you:
import * as ImagePicker from 'expo-image-picker';
async function addPhoto(fromCamera: boolean) {
const options: ImagePicker.ImagePickerOptions = { mediaTypes: ['images'], quality: 0.7, allowsEditing: true };
if (fromCamera) {
const permission = await ImagePicker.requestCameraPermissionsAsync();
if (!permission.granted) return;
}
const result = fromCamera
? await ImagePicker.launchCameraAsync(options)
: await ImagePicker.launchImageLibraryAsync(options);
if (result.canceled) return;
const uri = result.assets[0].uri;
// copy into the app's document directory, then save the new path on the hike
}quality: 0.7 compresses the JPEG, which matters when a hike has twenty photos. Cancelling isn’t an error: the promise resolves with canceled: true. And the returned uri points into a cache directory the system may clear, so copy the file into your app’s document directory with expo-file-system and store that path in SQLite, as the storage lesson described. The iOS Simulator has no camera, so test camera capture on a real phone.
When you need your own camera interface, such as scanning QR codes on trail markers, use expo-camera, whose CameraView component renders a live preview inside your layout.
A reminder notification
Hikers forget to log hikes. A daily reminder at 7 pm, only on days they’ve turned it on, is a local notification: scheduled on the device, no server involved.
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: false,
shouldSetBadge: false,
}),
});
export async function enableDailyReminder() {
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('reminders', {
name: 'Hike reminders',
importance: Notifications.AndroidImportance.DEFAULT,
});
}
const { granted } = await Notifications.requestPermissionsAsync();
if (!granted) return false;
await Notifications.cancelAllScheduledNotificationsAsync();
await Notifications.scheduleNotificationAsync({
content: { title: 'Out on a trail today?', body: 'Log it while you remember the details.', data: { url: '/new-hike' } },
trigger: { type: Notifications.SchedulableTriggerInputTypes.DAILY, hour: 19, minute: 0, channelId: 'reminders' },
});
return true;
}setNotificationHandler decides whether a notification shows while the app is open. On Android, notifications belong to channels that users can mute individually in Settings, so create a clearly named one. Cancelling before scheduling avoids stacking duplicate reminders each time the setting is toggled.
The data.url pays off with Expo Router: in the root layout, listen with Notifications.addNotificationResponseReceivedListener, read response.notification.request.content.data.url, and call router.push(url). Tapping the reminder then opens the add-hike modal directly, through the same routes your deep links use.
Push notifications, sent from a server, need more: a development build, credentials for Apple’s and Google’s push services (EAS manages them), and a push token sent to your backend. The scheduling and handling code you just wrote stays the same.
Section 5 is about getting all of this into users’ hands: debugging, testing and shipping.
Key takeaways
watchPositionAsyncstreams positions until you callremove()on the subscription it returns, so remove it in the effect’s cleanup.- Accuracy and update intervals trade precision for battery; choose the least precise settings the feature can live with.
- GPS readings are noisy, so drop points with poor accuracy before computing distance.
- expo-image-picker returns
canceledplus anassetsarray; picked files start in a cache, so copy anything you keep. - Local notifications are scheduled on the device with a trigger, and
datacan carry the route to open when the user taps them.