Adding maps to a mobile app is one of those features that sounds simple enough — until you actually try to build it. Between platform-specific API keys, permission flows that differ on iOS and Android, marker rendering performance, and just figuring out which library to use in the first place, it gets complicated fast. And honestly, if you search for "React Native maps" right now, most of what you'll find is either outdated, covers only one piece of the puzzle, or glosses over the gotchas that end up costing you hours in production.
I've been through that cycle more times than I'd like to admit.
This guide covers everything you need to build a production-ready map experience in a React Native app using Expo. We'll set up Google Maps on both platforms, show the user's live location, render custom markers with callouts, cluster hundreds of pins without tanking performance, draw directions between two points, and even implement geofencing — all with working code you can drop into your project today.
Choosing a Map Library: react-native-maps vs expo-maps
Before writing any code, you need to pick a library. In the Expo ecosystem in 2026, there are really only two options worth considering.
react-native-maps is the battle-tested choice. It's been around for years, has a huge community, and supports both Apple Maps and Google Maps on iOS plus Google Maps on Android. It works inside Expo Go without any additional setup, which makes prototyping super fast. Version 1.20.x works with the New Architecture through the interop layer, and there's an ongoing effort in version 1.21.0+ to go fully New Architecture-first.
expo-maps is Expo's own native maps library. It uses Apple Maps on iOS and Google Maps on Android — but it intentionally doesn't support Google Maps on iOS. It's still in alpha, meaning breaking changes happen frequently. It also requires a development build (no Expo Go support), and certain event handlers like onMarkerClick need iOS 18.0 or later.
For most production apps in 2026, react-native-maps is the right call. It's mature, well-documented, and the ecosystem of add-on libraries (clustering, directions, heatmaps) is built around it. This guide uses react-native-maps throughout, with notes on expo-maps where relevant.
Setting Up react-native-maps with Expo
Install the library using the Expo CLI, which handles version compatibility with your current SDK automatically:
npx expo install react-native-maps
That's it for basic development. When testing inside Expo Go, the map renders using Apple Maps on iOS and Google Maps on Android — no API key required. However, deploying to the app stores does require a Google Maps API key, which we'll set up next.
Configuring Google Maps API Keys
To use Google Maps in production, you need API keys from the Google Cloud Console. Here's the step-by-step process for both platforms.
Step 1: Enable the Maps SDKs
Open the Google Cloud Console, select or create a project, then navigate to APIs & Services and enable both Maps SDK for Android and Maps SDK for iOS.
Step 2: Create API Keys
For Android, create an API key restricted to your app's package name and SHA-1 certificate fingerprint. You can find the SHA-1 in the Google Play Console under App integrity → Play app signing. One thing that trips people up: you need to upload an initial build to Google Play before this fingerprint is actually available.
For iOS, create an API key restricted to your app's bundle identifier.
Step 3: Add Keys to Your Expo Config
Add the keys through the react-native-maps config plugin in your app.json or app.config.js:
{
"expo": {
"plugins": [
[
"react-native-maps",
{
"androidGoogleMapsApiKey": "YOUR_ANDROID_API_KEY",
"iosGoogleMapsApiKey": "YOUR_IOS_API_KEY"
}
]
]
}
}
Store your keys in environment variables or a .env file rather than hardcoding them. After adding the keys, rebuild your development binary with eas build or npx expo run:android / npx expo run:ios — config plugin changes aren't picked up by hot reload.
Displaying a Map with MapView
With the library installed, rendering a map takes just a few lines. Here's the key detail most tutorials skip over: MapView needs explicit dimensions. Without them, it renders with zero height and you'll just see a blank screen wondering what went wrong.
import MapView, { PROVIDER_GOOGLE, Marker } from 'react-native-maps';
import { StyleSheet, View } from 'react-native';
const INITIAL_REGION = {
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
};
export default function MapScreen() {
return (
<View style={styles.container}>
<MapView
style={styles.map}
provider={PROVIDER_GOOGLE}
initialRegion={INITIAL_REGION}
showsUserLocation
showsMyLocationButton
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
map: { width: '100%', height: '100%' },
});
Setting provider={PROVIDER_GOOGLE} forces Google Maps on both platforms. Without it, iOS defaults to Apple Maps. The showsUserLocation prop renders the familiar blue dot at the user's position, and showsMyLocationButton adds a native button to recenter the map.
Switching Map Types
Google Maps supports several map types through the mapType prop: standard (the default road map), satellite, hybrid (satellite with roads overlay), and terrain. Switching is as simple as adding mapType="hybrid" to your MapView.
Getting User Location with expo-location
While showsUserLocation shows the blue dot, you'll often need the actual coordinates — to center the map, calculate distances, or send them to a backend. For that, you need expo-location.
npx expo install expo-location
Here's a custom hook that handles permissions and returns the user's current coordinates:
import { useState, useEffect } from 'react';
import * as Location from 'expo-location';
export function useUserLocation() {
const [location, setLocation] = useState<Location.LocationObjectCoords | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setError('Location permission denied');
return;
}
const current = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High,
});
setLocation(current.coords);
})();
}, []);
return { location, error };
}
Use it in your map component to center on the user automatically:
export default function MapScreen() {
const { location } = useUserLocation();
const mapRef = useRef<MapView>(null);
useEffect(() => {
if (location && mapRef.current) {
mapRef.current.animateToRegion({
latitude: location.latitude,
longitude: location.longitude,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
}, 1000);
}
}, [location]);
return (
<MapView
ref={mapRef}
style={{ flex: 1 }}
provider={PROVIDER_GOOGLE}
initialRegion={INITIAL_REGION}
showsUserLocation
/>
);
}
Tracking Location in Real Time
For ride-sharing, delivery, or fitness apps, you need continuous location updates. Use Location.watchPositionAsync instead of getCurrentPositionAsync:
useEffect(() => {
let subscription: Location.LocationSubscription;
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') return;
subscription = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
distanceInterval: 10, // update every 10 meters
timeInterval: 5000, // or every 5 seconds
},
(newLocation) => {
setLocation(newLocation.coords);
}
);
})();
return () => subscription?.remove();
}, []);
Adding Custom Markers and Callouts
Default red pins are fine for demos, but real apps need custom markers. You can use images, SVGs, or entirely custom React Native views as markers.
Basic Markers with Data
const places = [
{ id: '1', title: 'Golden Gate Bridge', description: 'Iconic suspension bridge', latitude: 37.8199, longitude: -122.4783 },
{ id: '2', title: 'Alcatraz Island', description: 'Former federal penitentiary', latitude: 37.8267, longitude: -122.4230 },
{ id: '3', title: 'Fisherman\'s Wharf', description: 'Waterfront dining and shopping', latitude: 37.8080, longitude: -122.4177 },
];
<MapView style={{ flex: 1 }} provider={PROVIDER_GOOGLE} initialRegion={INITIAL_REGION}>
{places.map((place) => (
<Marker
key={place.id}
coordinate={{ latitude: place.latitude, longitude: place.longitude }}
title={place.title}
description={place.description}
/>
))}
</MapView>
Custom Marker Icons
Use the image prop to replace the default pin with a custom image. Keep the image small — around 40x40 to 60x60 pixels works best for performance:
<Marker
coordinate={{ latitude: 37.8199, longitude: -122.4783 }}
image={require('./assets/custom-pin.png')}
title="Golden Gate Bridge"
/>
Custom Callouts
For richer info bubbles, use the Callout component to render any React Native view when a marker is tapped:
import { Marker, Callout } from 'react-native-maps';
import { View, Text, Image, StyleSheet } from 'react-native';
<Marker coordinate={{ latitude: 37.8199, longitude: -122.4783 }}>
<Callout tooltip>
<View style={styles.callout}>
<Text style={styles.calloutTitle}>Golden Gate Bridge</Text>
<Text style={styles.calloutText}>Opened in 1937. Span: 1,280m</Text>
</View>
</Callout>
</Marker>
Drawing Polylines and Polygons
Polylines let you draw paths, routes, or boundaries on the map. Import Polyline and Polygon from react-native-maps and pass them as children of MapView:
import MapView, { Polyline, Polygon } from 'react-native-maps';
const routeCoords = [
{ latitude: 37.7749, longitude: -122.4194 },
{ latitude: 37.7849, longitude: -122.4094 },
{ latitude: 37.7949, longitude: -122.4294 },
];
const parkBoundary = [
{ latitude: 37.7694, longitude: -122.4862 },
{ latitude: 37.7694, longitude: -122.4530 },
{ latitude: 37.7834, longitude: -122.4530 },
{ latitude: 37.7834, longitude: -122.4862 },
];
<MapView style={{ flex: 1 }} provider={PROVIDER_GOOGLE} initialRegion={INITIAL_REGION}>
<Polyline
coordinates={routeCoords}
strokeWidth={4}
strokeColor="#4285F4"
lineCap="round"
lineJoin="round"
/>
<Polygon
coordinates={parkBoundary}
fillColor="rgba(66, 133, 244, 0.2)"
strokeColor="#4285F4"
strokeWidth={2}
/>
</MapView>
Implementing Marker Clustering
Once your map has more than about 50 markers, you'll start noticing performance take a hit — especially with custom marker images. Clustering groups nearby markers into a single indicator showing a count, then expands when the user zooms in.
The most reliable Expo-compatible option is react-native-map-clustering, which wraps react-native-maps and uses Mapbox's Supercluster algorithm under the hood:
npx expo install react-native-map-clustering
Replace MapView with ClusterMap and render your markers as children. It's a surprisingly smooth swap:
import ClusterMap from 'react-native-map-clustering';
import { Marker, PROVIDER_GOOGLE } from 'react-native-maps';
const markers = generateMarkers(500); // array of { id, latitude, longitude }
export default function ClusteredMapScreen() {
return (
<ClusterMap
style={{ flex: 1 }}
provider={PROVIDER_GOOGLE}
initialRegion={INITIAL_REGION}
clusterColor="#4285F4"
clusterTextColor="#ffffff"
radius={50}
maxZoom={16}
>
{markers.map((m) => (
<Marker
key={m.id}
coordinate={{ latitude: m.latitude, longitude: m.longitude }}
tracksViewChanges={false}
/>
))}
</ClusterMap>
);
}
Custom Cluster Rendering
The default cluster bubble is functional but pretty plain. You can override it with renderCluster:
<ClusterMap
style={{ flex: 1 }}
provider={PROVIDER_GOOGLE}
initialRegion={INITIAL_REGION}
renderCluster={(cluster) => {
const { id, geometry, onPress, properties } = cluster;
const count = properties.point_count;
return (
<Marker
key={`cluster-${id}`}
coordinate={{
latitude: geometry.coordinates[1],
longitude: geometry.coordinates[0],
}}
onPress={onPress}
>
<View style={[styles.cluster, { width: 30 + count, height: 30 + count }]}>
<Text style={styles.clusterText}>{count}</Text>
</View>
</Marker>
);
}}
>
{/* markers */}
</ClusterMap>
Clustering Performance Tips
- Always set
tracksViewChanges={false}on markers that use static images — this prevents React Native from continuously re-rendering the marker view. Seriously, this one prop change can be night and day. - Give each marker a stable, unique
keybased on an ID rather than an array index. This lets React recycle views efficiently. - Use the
onMarkerPresscallback on theMapViewlevel instead ofonPresson individual markers when dealing with large datasets. - Tune the
radius(default 40) andmaxZoom(default 20) props to control how aggressively clustering groups pins at each zoom level.
Adding Route Directions Between Two Points
Drawing a route between an origin and destination requires the Google Directions API. The easiest approach is the react-native-maps-directions library, which handles the API call and polyline rendering in one neat component:
npx expo install react-native-maps-directions
Make sure the Directions API is enabled in your Google Cloud Console project (it's a separate API from the Maps SDK — easy to miss).
import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps';
import MapViewDirections from 'react-native-maps-directions';
const GOOGLE_MAPS_API_KEY = 'YOUR_API_KEY';
const origin = { latitude: 37.7749, longitude: -122.4194 };
const destination = { latitude: 37.8199, longitude: -122.4783 };
export default function DirectionsScreen() {
const mapRef = useRef<MapView>(null);
return (
<MapView
ref={mapRef}
style={{ flex: 1 }}
provider={PROVIDER_GOOGLE}
initialRegion={INITIAL_REGION}
>
<Marker coordinate={origin} title="Start" />
<Marker coordinate={destination} title="End" />
<MapViewDirections
origin={origin}
destination={destination}
apikey={GOOGLE_MAPS_API_KEY}
strokeWidth={5}
strokeColor="#4285F4"
mode="DRIVING"
onReady={(result) => {
mapRef.current?.fitToCoordinates(result.coordinates, {
edgePadding: { top: 80, right: 80, bottom: 80, left: 80 },
animated: true,
});
}}
/>
</MapView>
);
}
The onReady callback fires once the route is calculated. The result object includes distance (in km), duration (in minutes), and coordinates (the polyline points). The mode prop accepts DRIVING, WALKING, BICYCLING, or TRANSIT.
Manual Directions with the Directions API
If you want more control — say, to show turn-by-turn instructions or multiple route alternatives — you can call the Directions API directly and decode the polyline yourself:
import polyline from '@mapbox/polyline';
async function fetchRoute(origin, destination, apiKey) {
const url = `https://maps.googleapis.com/maps/api/directions/json?origin=${origin.latitude},${origin.longitude}&destination=${destination.latitude},${destination.longitude}&key=${apiKey}`;
const response = await fetch(url);
const data = await response.json();
if (data.routes.length === 0) return null;
const route = data.routes[0];
const points = polyline.decode(route.overview_polyline.points);
const coordinates = points.map(([lat, lng]) => ({
latitude: lat,
longitude: lng,
}));
return {
coordinates,
distance: route.legs[0].distance.text,
duration: route.legs[0].duration.text,
};
}
Install the decoder with npx expo install @mapbox/polyline and then pass the decoded coordinates to a Polyline component.
Building a Geofence
Geofencing creates a virtual boundary around a real-world location and triggers actions when a user enters or exits that area. It's one of those features that sounds fancy but is actually pretty straightforward to implement. Think delivery apps, location-based reminders, or attendance tracking.
First, render the geofence visually using a Circle overlay:
import MapView, { Circle, Marker, PROVIDER_GOOGLE } from 'react-native-maps';
const GEOFENCE = {
center: { latitude: 37.78825, longitude: -122.4324 },
radius: 300, // meters
};
<MapView style={{ flex: 1 }} provider={PROVIDER_GOOGLE} initialRegion={INITIAL_REGION}>
<Circle
center={GEOFENCE.center}
radius={GEOFENCE.radius}
fillColor="rgba(66, 133, 244, 0.15)"
strokeColor="#4285F4"
strokeWidth={2}
/>
<Marker coordinate={GEOFENCE.center} title="Geofence Center" />
</MapView>
Then combine it with live location tracking to detect when the user crosses the boundary:
import { getDistance } from 'geolib';
function checkGeofence(userCoords, geofence) {
const distance = getDistance(
{ latitude: userCoords.latitude, longitude: userCoords.longitude },
{ latitude: geofence.center.latitude, longitude: geofence.center.longitude }
);
return distance <= geofence.radius;
}
// Inside your location watcher callback:
const isInside = checkGeofence(newLocation.coords, GEOFENCE);
if (isInside && !wasInsideRef.current) {
console.log('User entered the geofence');
// trigger notification, API call, etc.
}
wasInsideRef.current = isInside;
Install geolib with npx expo install geolib. It provides accurate Haversine distance calculations so you don't have to implement the math yourself (and trust me, you don't want to).
Performance Optimization Tips
Maps are one of the most GPU-intensive components in a React Native app. Here are the practices that actually make a noticeable difference:
- Set
tracksViewChanges={false}on every marker that uses a static image or custom view. This is the single biggest performance win you can get. When set totrue(the default), React Native re-renders the marker image every single frame. - Use clustering for anything beyond 50 markers. Even if your dataset is only 100 items, clustering makes the map feel noticeably smoother on lower-end Android devices.
- Debounce
onRegionChangeCompletecallbacks. If you're fetching data based on the visible map region, debounce by at least 300ms to avoid firing dozens of requests while the user pans around. - Optimize marker images. Keep PNGs under 5KB each. Oversized images cause visible lag when the map needs to render many markers at once.
- Avoid unnecessary re-renders of MapView. Memoize the component and make sure parent state changes don't cause the entire map to unmount and remount.
React.memoand stable references for region and marker data go a long way here. - Test on real devices. The iOS Simulator and Android emulator handle maps way better than most mid-range phones. Always benchmark on actual hardware before shipping.
New Architecture Compatibility in 2026
If you're on Expo SDK 55, the New Architecture is mandatory — you can't disable it. Here's what that means for maps:
react-native-mapsversion 1.20.x (the SDK 53 default) works through the New Architecture interop layer. For most features — rendering maps, markers, polylines, callouts — it's stable and production-ready.- Version 1.21.0 introduced a New Architecture-first rewrite, but it's still stabilizing. Keep an eye on the GitHub issues before upgrading.
- There's a known issue with Google Maps on iOS in SDK 55 where the Expo plugin can fail during prebuild. If you hit the
"Cannot add Google Maps to the project's AppDelegate"error, check for updates to the config plugin or consider using Apple Maps on iOS as a fallback. expo-mapsis built for the New Architecture from the ground up and doesn't have interop layer concerns — but remember, it's still in alpha.
Frequently Asked Questions
Do I need a Google Maps API key to test in Expo Go?
Nope. Expo Go uses Apple Maps on iOS and a default Google Maps setup on Android, so you can develop and test without an API key. You only need keys when creating a production build for the app stores.
Why is my map showing a blank screen on Android?
This usually means the Google Maps API key is missing, invalid, or the Maps SDK for Android isn't enabled in your Google Cloud project. Check the API Console to verify the key is active and that the correct SHA-1 fingerprint is listed under key restrictions. Also make sure you've rebuilt the native binary after adding the key to your config plugin — this is the one people forget most often.
Can I use Apple Maps on iOS and Google Maps on Android?
Yes, and this is actually the default behavior of react-native-maps when you don't set provider={PROVIDER_GOOGLE}. On iOS it uses Apple Maps (MapKit), and on Android it uses Google Maps. This approach skips the iOS Google Maps API key setup entirely and is a totally valid strategy for production if you don't need a consistent map style across platforms.
How do I handle map permissions being denied?
The map itself renders just fine without location permission — it simply won't show the user's blue dot or center on their location. Handle denied permissions gracefully by showing a fallback UI or a message explaining why the app works better with location access. You can use Linking.openSettings() to give users a direct path to the device settings if they want to enable it later.
Is react-native-maps compatible with Expo SDK 55 and the New Architecture?
Yes, through the interop layer. Version 1.20.x works reliably for most features. There is a known issue with the Google Maps iOS config plugin on SDK 55 though, so keep an eye on the react-native-maps GitHub repository and keep dependencies up to date. If you're starting a fresh project and can target iOS 17+, expo-maps is a future-proof alternative worth evaluating — just be aware of its alpha status.