TL;DR
- Pick
react-native-mapsoverexpo-mapsfor most apps in 2026. The ecosystem (docs, answers, LLM snippets) is the real advantage. - A blank map is almost always an unsized parent view or an API key missing from the release build.
- Request location permission on user action, never on launch. Apple rejects the splash-screen prompt.
- Always call
remove()onwatchPositionAsyncsubscriptions, or you leak GPS listeners and drain battery. - Above a few hundred markers, cluster. Custom
<Marker>children are the #1 cause of slow maps.
Adding a map to a React Native app looks easy until you ship it. Then you meet API keys that only fail in release builds, permission prompts that fail review, and marker lists that turn your framerate into a slideshow. Here's how react-native-maps actually behaves in production.
What react-native-maps actually is
It's a cross-platform library that renders native map views: Google Maps on Android, and Apple Maps or Google Maps on iOS. You get a declarative MapView plus Marker, Polyline, Polygon, and Circle. Gestures, tiles, and rendering all run natively, not in JS.
Why it's the default: it works in Expo Go, it was originally built at Airbnb, and it has years of production mileage behind it.
react-native-maps vs expo-maps
Expo shipped expo-maps, built on SwiftUI and Jetpack Compose. The honest comparison:
| Concern | react-native-maps | expo-maps |
|---|---|---|
| Providers | Google + Apple (both on iOS) | Apple on iOS, Google on Android |
| Expo Go | Yes | No (dev client / EAS build) |
| Setup | Config plugin + API keys | Config plugin, cleaner defaults |
| Community | Huge, years of answers | Small, growing |
| iOS target | Older versions supported | Newer iOS versions |
| Native feel | Good | Excellent |
| Many markers | Needs clustering | Similar limits |
My take: start with react-native-maps unless you need the newest platform look. When you hit a weird bug at 11pm before release, the ecosystem matters more than anything else in this table.
Setup with Expo
npx expo install react-native-maps expo-location
Keys come from the Google Maps Platform and go in app.json:
{
"expo": {
"ios": {
"config": { "googleMapsApiKey": "YOUR_IOS_KEY" },
"infoPlist": {
"NSLocationWhenInUseUsageDescription": "We show nearby places on a map.",
"NSLocationAlwaysAndWhenInUseUsageDescription": "We sync your route in the background so you can see the full trip."
}
},
"android": {
"config": { "googleMaps": { "apiKey": "YOUR_ANDROID_KEY" } },
"permissions": ["ACCESS_FINE_LOCATION", "ACCESS_COARSE_LOCATION"]
}
}
}
Gotchas, in the order they'll bite you:
- Restrict keys per platform. iOS by bundle ID, Android by SHA-1. Unrestricted keys are an abuse risk once you're live.
-
Blank map in TestFlight but fine in dev? The key isn't wired into the release build. Rebuild after any
app.jsonchange. -
Every
NSLocation*string must explain why in user language. "We need your location" gets rejected.
Rendering a map and markers
import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps';
import { StyleSheet, View } from 'react-native';
export default function LocationScreen() {
return (
<View style={{ flex: 1 }}>
<MapView
provider={PROVIDER_GOOGLE}
style={StyleSheet.absoluteFill}
initialRegion={{
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
>
<Marker
coordinate={{ latitude: 37.78825, longitude: -122.4324 }}
title="Union Square"
description="Downtown San Francisco"
/>
</MapView>
</View>
);
}
Things that trip up first builds:
-
initialRegionvsregion. UseinitialRegionfor maps users pan freely.regionre-centers on every render and fights gestures. -
Deltas set zoom, not radius. Roughly
0.01= city block,0.1= neighborhood,1.0= metro. There's nozoomprop. -
PROVIDER_GOOGLEforces Google tiles on iOS. Omit it to fall back to Apple Maps. - Unsized parent = zero-height map. This is the #1 "my map is blank" bug.
Location permissions without getting rejected
import * as Location from 'expo-location';
async function getCurrentPosition() {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
return null; // Show a soft explainer, not a hard error.
}
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced,
});
return location.coords;
}
Three rules:
- Never request on launch. Trigger it from a "Show my location" button or a screen that visibly needs it.
- Foreground before background. iOS enforces the order.
-
Accuracy.Balancedis the right default.BestForNavigationis for driving/running apps and eats battery.
Live tracking, with the cleanup everyone forgets:
import { useEffect, useState } from 'react';
import * as Location from 'expo-location';
export function useLivePosition() {
const [position, setPosition] = useState<Location.LocationObjectCoords | null>(null);
useEffect(() => {
let subscription: Location.LocationSubscription | undefined;
(async () => {
subscription = await Location.watchPositionAsync(
{ accuracy: Location.Accuracy.Balanced, timeInterval: 3000, distanceInterval: 5 },
(location) => setPosition(location.coords),
);
})();
return () => subscription?.remove(); // skip this and you leak GPS listeners
}, []);
return position;
}
Routes, polylines, and geofences
import { Polyline, Circle } from 'react-native-maps';
<Polyline
coordinates={route} // array of { latitude, longitude }
strokeColor="#2563eb"
strokeWidth={4}
/>
<Circle
center={{ latitude, longitude }}
radius={500} // meters
fillColor="rgba(37, 99, 235, 0.15)"
strokeColor="#2563eb"
/>
react-native-maps doesn't compute routes. Call the Google Directions API or Apple MapKit Directions and feed the coordinates into a Polyline. react-native-maps-directions wraps the Google call, but check Google's billing before you scale.
Geofencing lives in expo-location's startGeofencingAsync. It uses OS-level geofence APIs and keeps working when the app is backgrounded.
Marker clustering: the performance cliff
Every Marker is a native view. A hundred is fine. A few hundred gets choppy on mid-range Android. Thousands can crash with out-of-memory errors.
Options, most pragmatic first:
-
react-native-map-clustering: drop-in
MapViewreplacement using supercluster. - Server-side clustering: for very large datasets, cluster on the backend and send only the visible viewport.
-
Bitmap markers: an
imageprop pointing to a PNG is much cheaper than a custom React child.
If you do need custom children, memoize them:
import React, { memo } from 'react';
import { View, Text } from 'react-native';
import { Marker } from 'react-native-maps';
const PriceMarker = memo(function PriceMarker({
coordinate,
price,
}: {
coordinate: { latitude: number; longitude: number };
price: string;
}) {
return (
<Marker coordinate={coordinate} tracksViewChanges={false}>
<View style={{ backgroundColor: '#2563eb', padding: 4, borderRadius: 6 }}>
<Text style={{ color: 'white', fontWeight: '600' }}>{price}</Text>
</View>
</Marker>
);
});
A slow map is almost always a re-rendering-markers problem, not a tile problem. And test on a mid-range Android device, not your flagship iPhone.
Skipping the boilerplate
Wiring MapView, markers, permissions, routes, and the "location denied" state is a few hundred lines and half a day of "why is my map blank." It barely changes between a delivery app, a fitness app, and a real-estate app.
That's the kind of scaffold AI builders handle well. If you describe "a food delivery app where couriers see nearby orders on a map" to RapidNative, it generates an Expo project with react-native-maps, expo-location permission strings, and a marker list bound to a data model. It's real React Native code you can export and edit by hand, so you spend your time on the parts that differentiate your app.
FAQ
Why is my map blank? Check in order: parent view has no height, API key not in the release build, key restricted to the wrong bundle ID, config plugin missing in app.json.
Is react-native-maps free? The library is MIT licensed. Google Maps usage is billed through the Google Maps Platform beyond its free usage allowance. Apple Maps on iOS has no usage fee.
Does it work in Expo Go? Yes. expo-maps needs a custom dev client.
Background tracking? NSLocationAlwaysAndWhenInUseUsageDescription + UIBackgroundModes: location on iOS, ACCESS_BACKGROUND_LOCATION on Android, then startLocationUpdatesAsync with a registered task. Foreground permission first, and be ready to justify it in review.
Your turn
What's the worst map bug you've shipped? Blank TestFlight map, marker slideshow, or something weirder? Drop it in the comments, along with what you're building.
Top comments (0)