Location
Accessing the user’s location on React Native: Expo Location for managed projects, react-native-geolocation-service for bare RN. Handle permissions with a rationale, request high-accuracy only when needed, and design for permission denial since users frequently say no.
Permissions, watch, background, accuracy
EXAMPLE
// 1) Install — Expo
// npx expo install expo-location
import * as Location from 'expo-location';
import { useEffect, useState } from 'react';
import { Text, Button, View, Platform } from 'react-native';
// 2) Request permission + read once
export function CurrentLocation() {
const [location, setLocation] = useState(null);
const [error, setError] = useState(null);
async function request() {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setError('Permission denied');
return;
}
const pos = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced,
});
setLocation(pos);
}
return (
<View>
<Button title="Get location" onPress={request} />
{error && <Text>{error}</Text>}
{location && (
<Text>
{location.coords.latitude.toFixed(4)},
{location.coords.longitude.toFixed(4)}
(±{location.coords.accuracy?.toFixed(0)}m)
</Text>
)}
</View>
);
}
// 3) Accuracy levels
// Location.Accuracy.Lowest ≈ 3 km (cheapest)
// Location.Accuracy.Low ≈ 1 km
// Location.Accuracy.Balanced ≈ 100 m
// Location.Accuracy.High ≈ 10 m
// Location.Accuracy.Highest ≈ best available
// Location.Accuracy.BestForNavigation (use only for active turn-by-turn)
// Higher accuracy = more battery + GPS chip on.
// 4) Watch position changes
import { useEffect, useRef } from 'react';
export function WatchLocation() {
const subscription = useRef(null);
useEffect(() => {
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') return;
subscription.current = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.Balanced,
timeInterval: 5000, // ms between updates
distanceInterval: 10, // metres
},
(position) => {
console.log(position.coords);
},
);
})();
return () => { subscription.current?.remove(); };
}, []);
return null;
}
// 5) Permission flow with rationale
async function requestWithRationale() {
const { status: existing } = await Location.getForegroundPermissionsAsync();
if (existing === 'granted') return true;
// Show your own rationale UI before requesting:
const userAccepted = await showRationaleModal();
if (!userAccepted) return false;
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
// User denied. On second request, OS may not show prompt — guide to Settings:
Linking.openSettings();
return false;
}
return true;
}
// Users who decline twice typically can't be re-prompted by the system — direct to Settings.
// 6) Reverse geocoding — coords → address
const [address] = await Location.reverseGeocodeAsync({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
});
console.log(`${address.city}, ${address.region}, ${address.country}`);
// Forward geocoding — address → coords
const results = await Location.geocodeAsync('Sydney Opera House, Australia');
console.log(results[0]); // { latitude, longitude, ... }
// 7) Distance calculation — Haversine
function haversine(a, b) {
const R = 6371e3;
const toRad = (d) => (d * Math.PI) / 180;
const dLat = toRad(b.lat - a.lat);
const dLon = toRad(b.lng - a.lng);
const s = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a.lat)) * Math.cos(toRad(b.lat)) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.atan2(Math.sqrt(s), Math.sqrt(1 - s));
}
haversine({ lat: -33.86, lng: 151.21 }, { lat: -33.87, lng: 151.22 }); // metres
// 8) Background location (USE SPARINGLY — review-board sensitive)
import * as TaskManager from 'expo-task-manager';
TaskManager.defineTask('BACKGROUND_LOCATION', async ({ data, error }) => {
if (error) return console.error(error);
if (data) {
const { locations } = data;
// Send to server / store locally
}
});
async function startBackground() {
const { status } = await Location.requestBackgroundPermissionsAsync();
if (status !== 'granted') return;
await Location.startLocationUpdatesAsync('BACKGROUND_LOCATION', {
accuracy: Location.Accuracy.Balanced,
timeInterval: 60_000,
distanceInterval: 50,
foregroundService: {
notificationTitle: 'Tracking your run',
notificationBody: 'We are recording your location',
},
});
}
// Justify the permission to users + Apple/Google reviewers. Misuse → app rejection.
// 9) Geofencing
import * as Location from 'expo-location';
TaskManager.defineTask('GEOFENCE', ({ data, error }) => {
if (error) return;
const { eventType, region } = data;
if (eventType === Location.GeofencingEventType.Enter) {
console.log('entered', region.identifier);
}
});
await Location.startGeofencingAsync('GEOFENCE', [
{ identifier: 'home', latitude: -33.86, longitude: 151.21, radius: 200 },
{ identifier: 'gym', latitude: -33.87, longitude: 151.23, radius: 100 },
]);
// 10) iOS permission strings — Info.plist
// <key>NSLocationWhenInUseUsageDescription</key>
// <string>We use your location to find nearby venues</string>
// <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
// <string>We track your location for delivery updates</string>
// <key>NSLocationAlwaysUsageDescription</key>
// <string>Background updates for safety alerts</string>
// Expo: app.json
{
"expo": {
"plugins": [[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "We track your location for delivery updates.",
},
]],
},
}
// 11) Android manifest
// <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
// <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
// <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
// 12) Approximate vs precise (iOS 14+ / Android 12+)
// • User can grant 'approximate only' → ±5km accuracy
// • Detect: pos.coords.accuracy > 1000 likely means approximate
// • Provide downgrade UX for users who don't want precise sharing
// 13) Map UI — react-native-maps
// npm install react-native-maps
import MapView, { Marker } from 'react-native-maps';
<MapView style={{ flex: 1 }} initialRegion={{ latitude: -33.86, longitude: 151.21, latitudeDelta: 0.01, longitudeDelta: 0.01 }}>
<Marker coordinate={{ latitude: -33.86, longitude: 151.21 }} title="Sydney" />
</MapView>
// 14) Common bugs
// • Forgetting iOS Info.plist strings → silent denial / app crash
// • Polling location every second → battery drain
// • Not handling permission denial → blank UI
// • Web build but no HTTPS → 'User denied geolocation' from Chrome (HTTPS required)
// • Background mode declared but no actual background updates → review board rejection
// • Not asking 'when in use' before 'always' → Android rejects the flow
// • Caching last location forever → user moves; show stale data
// • Calling requestForegroundPermissions inside render → infinite re-renders
// • Coords from emulator are usually (0, 0) — test on real device
// • Reverse geocoding rate-limited → cache results, throttle calls
Why it matters
Use expo-location (managed) or react-native-geolocation-service (bare). Default to Accuracy.Balanced + watchPositionAsync; tighten to BestForNavigation only when driving turn-by-turn. Show a rationale before requesting, declare every usage string up front, and resist background tracking unless your feature genuinely needs it — app stores reject misuse.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import * as Location from 'expo-location';
await Location.requestForegroundPermissionsAsync();
const { coords } = await Location.getCurrentPositionAsync();
console.log(coords.latitude, coords.longitude);
Try it Yourself »
Discussion
Loading…