Every tutorial makes react-native-maps look easy. npx expo install, drop a <MapView>, done. Then you ship, and you meet the real map. The one with API keys that fail only in TestFlight, permission prompts that Apple rejects, and marker lists that turn your framerate into a slideshow.
Here are the seven things that would have saved me a week of debugging on my first production map screen.
1. The blank map is almost always a height problem
// Won't render. Parent has no explicit height.
<View>
<MapView style={{ flex: 1 }} />
</View>
// Works.
<View style={{ flex: 1 }}>
<MapView style={StyleSheet.absoluteFill} />
</View>
MapView needs an ancestor with a resolved height. flex: 1 inside a zero-height parent silently fails. StyleSheet.absoluteFill is the safest pattern.
2. initialRegion vs region: pick one and don't switch
Use initialRegion for uncontrolled maps where the user pans freely. Use region only if you have a genuine reason to re-center on every render. Otherwise you're fighting user gestures and the map feels broken.
3. API keys must be restricted per platform
An unrestricted "any bundle ID" key works in dev and gets rotated by Google abuse detection within a week of launch. Restrict:
- iOS: by bundle identifier
- Android: by package name + SHA-1 fingerprint
Do this on day 1. Rotating a key that's already in the wild is painful.
4. Location permission ordering matters
const fg = await Location.requestForegroundPermissionsAsync();
if (fg.status !== 'granted') return;
const bg = await Location.requestBackgroundPermissionsAsync();
You cannot get background permission on iOS without foreground first. And do not request permission on app launch. Ask when the user taps a "show my location" button. Apple reviewers will reject an unexplained cold prompt.
5. Accuracy.BestForNavigation will kill your battery
Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced, // 👈 default for 99% of apps
});
BestForNavigation is for driving and running apps that literally need meter-level precision. It's roughly 10x the battery drain of Balanced. Apple reviewers do notice.
6. watchPositionAsync leaks if you don't clean it up
useEffect(() => {
let sub: Location.LocationSubscription | undefined;
(async () => {
sub = await Location.watchPositionAsync(
{ accuracy: Location.Accuracy.Balanced, distanceInterval: 5 },
(loc) => setPosition(loc.coords),
);
})();
return () => sub?.remove(); // 👈 THIS line
}, []);
Forget the sub?.remove() and every screen visit adds a permanent GPS listener. Users notice the battery drain within a day.
7. Every <Marker> is a native view
Above roughly 200 to 300 markers on Android, frames drop. Above 2,000, iOS runs out of memory. Two fixes:
-
Cluster with
react-native-map-clustering(a drop-inMapViewreplacement built on supercluster). -
Bitmap markers. Set
image={require('./pin.png')}instead of rendering React children. Native views for markers are cheap; React trees inside markers are expensive.
If you must render custom React markers, wrap them in React.memo and hoist all callbacks. Otherwise every pan re-renders every marker.
Bonus: expo-maps vs react-native-maps in 2026
Short version:
- react-native-maps: huge community, works in Expo Go, needs a config plugin. The default choice.
- expo-maps: SwiftUI and Jetpack Compose under the hood, faster startup, iOS 17+ only, no Expo Go. Good for greenfield apps.
For most teams shipping today, react-native-maps is still the safer bet, because every LLM, every Stack Overflow answer, and every AI code assistant still assumes it.
Speaking of which: the entire scaffold above (maps, permissions, markers, backend) can also be generated end to end from a prompt on RapidNative, if you'd rather skip the 400 lines of boilerplate and go straight to the differentiated part of your app.
Top comments (0)