expo-location
Foreground/background location, geofencing, and App Store disclosure strings - the expo-location cookbook for Expo SDK 57 field and delivery apps.
Search across all documentation pages
Foreground/background location, geofencing, and App Store disclosure strings - the expo-location cookbook for Expo SDK 57 field and delivery apps.
Quick-reference recipe card - copy-paste ready.
npx expo install expo-location// app.config.ts - disclosure strings (required for store review)
export default {
plugins: [
[
"expo-location",
{
locationWhenInUsePermission:
"FieldTrack shows nearby job sites while you use the app.",
locationAlwaysAndWhenInUsePermission:
"FieldTrack records visit coordinates in the background so supervisors see arrival times.",
isIosBackgroundLocationEnabled: true,
isAndroidBackgroundLocationEnabled: true,
},
],
],
};import * as Location from "expo-location";
export async function requestForegroundLocation(): Promise<boolean> {
const { status } = await Location.requestForegroundPermissionsAsync();
return status === "granted";
}
export async function watchPosition(onUpdate: (loc: Location.LocationObject) => void) {
const granted = await requestForegroundLocation();
if (!granted) return null;
return await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.Balanced,
distanceInterval: 25, // meters - reduce churn vs timeInterval alone
timeInterval: 10_000,
},
onUpdate
);
}When to reach for this:
When to avoid:
Foreground map pin, permission gate, and background upgrade flow with honest disclosure copy.
npx expo install expo-location// src/location/LocationPermissionGate.tsx
import { useState } from "react";
import { Linking, Pressable, Text, View } from "react-native";
import * as Location from "expo-location";
type Props = {
onGranted: () => void;
needsBackground?: boolean;
};
export function LocationPermissionGate({ onGranted, needsBackground }: Props) {
const [denied, setDenied] = useState(false);
async function request() {
const fg = await Location.requestForegroundPermissionsAsync();
if (fg.status !== "granted") {
setDenied(true);
return;
}
if (needsBackground) {
const bg = await Location.requestBackgroundPermissionsAsync();
if (bg.status !== "granted") {
setDenied(true);
return;
}
}
onGranted();
}
return (
<View style={{ padding: 24, gap: 12 }}>
<Text style={{ fontWeight: "600" }}>Location access</Text>
<Text>
We attach GPS to inspection photos so disputes resolve with proof of
on-site presence.
{needsBackground
? " Background access keeps visit logs accurate when you switch apps."
: ""}
</Text>
<Pressable onPress={request}>
<Text style={{ color: "#2563eb" }}>Continue</Text>
</Pressable>
{denied && (
<Pressable onPress={() => Linking.openSettings()}>
<Text>Open Settings</Text>
</Pressable>
)}
</View>
);
}// src/location/useCurrentPosition.ts
import { useEffect, useState } from "react";
import * as Location from "expo-location";
export function useCurrentPosition(enabled: boolean) {
const [coords, setCoords] = useState<Location.LocationObjectCoords | null>(
null
);
useEffect(() => {
if (!enabled) return;
let subscription: Location.LocationSubscription | null = null;
(async () => {
subscription = await Location.watchPositionAsync(
{ accuracy: Location.Accuracy.Balanced, distanceInterval: 20 },
(loc) => setCoords(loc.coords)
);
})();
return () => subscription?.remove();
}, [enabled]);
return coords;
}// src/location/getOneShotPosition.ts
import * as Location from "expo-location";
export async function getOneShotPosition() {
const { status } = await Location.getForegroundPermissionsAsync();
if (status !== "granted") return null;
return Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced,
});
}What this demonstrates:
app.config plugin strings.needsBackground is true.| Tier | iOS | Android | Use case |
|---|---|---|---|
| Foreground | When In Use | Fine location | Maps, one-shot tagging |
| Background | Always | Background + foreground | Visit logs while app backgrounded |
| Reduced accuracy | Approximate (iOS 14+) | Coarse optional | City-level features only |
Accuracy.Highest → GPS lock, heaviest battery
Accuracy.Balanced → Default for field apps
Accuracy.Low → City-level, cheapestdistanceInterval over aggressive timeInterval - fewer JS wakeups.stopLocationUpdatesAsync or remove watch subscription when screen unmounts.// src/location/geofenceTask.ts - global scope import at entry
import * as Location from "expo-location";
import * as TaskManager from "expo-task-manager";
export const GEOFENCE_TASK = "site-geofence";
TaskManager.defineTask(GEOFENCE_TASK, ({ data, error }) => {
if (error) {
console.error(error);
return;
}
const { eventType, region } = data as Location.GeofencingEvent;
if (eventType === Location.GeofencingEventType.Enter) {
console.log("Entered", region.identifier);
}
});await Location.startGeofencingAsync(GEOFENCE_TASK, [
{
identifier: "warehouse-a",
latitude: 37.7749,
longitude: -122.4194,
radius: 200,
notifyOnEnter: true,
notifyOnExit: false,
},
]);| Key | Platform | Example |
|---|---|---|
locationWhenInUsePermission | iOS plugin | Why foreground access helps |
locationAlwaysAndWhenInUsePermission | iOS plugin | Why background is necessary |
ACCESS_BACKGROUND_LOCATION | Android | Same narrative for reviewers |
| API | Purpose |
|---|---|
requestForegroundPermissionsAsync | When-in-use permission |
requestBackgroundPermissionsAsync | Always / background |
getCurrentPositionAsync | One-shot coordinate |
watchPositionAsync | Streaming updates |
startGeofencingAsync | Region enter/exit |
reverseGeocodeAsync | Coordinates → address (network) |
subscription.remove() in useEffect cleanup.TaskManager.defineTask at module scope.| Alternative | Use When | Don't Use When |
|---|---|---|
| Foreground only | Map and one-shot tagging | Proof of on-site while app backgrounded |
| Manual address entry | Privacy-sensitive users | Real-time fleet tracking |
| Geofencing | Site enter/exit alerts | Sub-meter precision indoors |
| Third-party maps SDK | Turn-by-turn navigation | Simple "where am I" pin |
npx expo install expo-locationAdd the expo-location config plugin with disclosure strings, then npx expo prebuild or EAS build.
isIosBackgroundLocationEnabled in the plugin and UIBackgroundModes location.Accuracy.Balanced - upgrade to Highest only for sub-10m requirements.import { Linking } from "react-native";
await Linking.openSettings();reverseGeocodeAsync uses network services. Cache last known label locally for offline display.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026