Skip to main content

Current location

Track the user's position with expo-location and display it on the map.

Permissions setup

Make sure expo-location is listed in the plugins array in your app.json (see Getting Started). The plugin automatically adds the required ACCESS_FINE_LOCATION permission to AndroidManifest.xml and NSLocationWhenInUseUsageDescription to Info.plist during prebuild.

Example

import type { Location } from '@maplibre/maplibre-react-native';
import { Camera, MapView, UserLocation } from '@maplibre/maplibre-react-native';
import * as ExpoLocation from 'expo-location';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Alert, Pressable, StyleSheet, Text, View } from 'react-native';
import { BARIKOI_COLORS, useBarikoiMapStyle } from '../utils/mapUtils';

const convertLocation = (loc: ExpoLocation.LocationObject): Location => ({
coords: {
latitude: loc.coords.latitude,
longitude: loc.coords.longitude,
altitude: loc.coords.altitude ?? undefined,
accuracy: loc.coords.accuracy ?? undefined,
heading: loc.coords.heading ?? undefined,
speed: loc.coords.speed ?? undefined,
},
timestamp: loc.timestamp,
});

export default function CurrentLocationScreen() {
const { styleJson, loading: mapLoading, error: mapError } = useBarikoiMapStyle();
const [userLocation, setUserLocation] = useState<Location | null>(null);
const [hasPermission, setHasPermission] = useState(false);
const [permissionLoading, setPermissionLoading] = useState(true);
const [hasFlownToLocation, setHasFlownToLocation] = useState(false);
const [followsUser, setFollowsUser] = useState(false);
const cameraRef = useRef<any>(null);

useEffect(() => {
let subscription: ExpoLocation.LocationSubscription;
const init = async () => {
try {
const { status } = await ExpoLocation.requestForegroundPermissionsAsync();
if (status === 'granted') {
setHasPermission(true);
const loc = await ExpoLocation.getCurrentPositionAsync({});
setUserLocation(convertLocation(loc));
subscription = await ExpoLocation.watchPositionAsync(
{ accuracy: ExpoLocation.Accuracy.High, timeInterval: 1000, distanceInterval: 10 },
(loc) => setUserLocation(convertLocation(loc)),
);
} else {
Alert.alert('Permission denied', 'Location permission is required.');
}
} finally {
setPermissionLoading(false);
}
};
init();
return () => { if (subscription) subscription.remove(); };
}, []);

useEffect(() => {
if (userLocation && (followsUser || !hasFlownToLocation) && cameraRef.current) {
const { latitude, longitude } = userLocation.coords;
cameraRef.current.setCamera({
centerCoordinate: [longitude, latitude],
zoomLevel: 16,
animationDuration: 2000,
animationMode: 'flyTo',
});
if (!hasFlownToLocation) setHasFlownToLocation(true);
}
}, [userLocation, hasFlownToLocation, followsUser]);

if (mapLoading || permissionLoading) {
return <View style={styles.centered}><ActivityIndicator size="large" color={BARIKOI_COLORS.primary} /></View>;
}
if (mapError) {
return <View style={styles.centered}><Text>{mapError}</Text></View>;
}

return (
<View style={styles.container}>
<MapView style={styles.map} attributionEnabled={false} zoomEnabled compassEnabled mapStyle={styleJson}>
<Camera ref={cameraRef} centerCoordinate={[90.364159, 23.823724]} zoomLevel={10} animationMode="flyTo" />
{hasPermission && (
<UserLocation
visible={true}
animated={true}
renderMode="normal"
showsUserHeadingIndicator={true}
minDisplacement={10}
onUpdate={(loc) => setUserLocation(loc)}
onPress={() => {
if (userLocation) Alert.alert('Location', `Lat: ${userLocation.coords.latitude}\nLng: ${userLocation.coords.longitude}`);
}}
/>
)}
</MapView>
<View style={styles.controls}>
<Pressable
style={[styles.controlBtn, followsUser && styles.controlBtnActive]}
onPress={() => setFollowsUser(prev => !prev)}
>
<Text style={{ fontSize: 18 }}></Text>
</Pressable>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: 'white' },
map: { flex: 1 },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
controls: { position: 'absolute', right: 16, bottom: 32 },
controlBtn: { width: 44, height: 44, borderRadius: 22, backgroundColor: 'white', justifyContent: 'center', alignItems: 'center', elevation: 5, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3.84 },
controlBtnActive: { backgroundColor: BARIKOI_COLORS.primary },
});

Key points

  • Convert between expo-location and MapLibre Location types with the convertLocation helper
  • Use watchPositionAsync for continuous tracking — not polling
  • Always remove the subscription on unmount to avoid memory leaks
  • Use cameraRef to fly to the user's location on first update