Every app I have shipped with a map went through the same three stages. Stage one, the map renders and everyone is delighted. Stage two, someone adds 400 markers and Android turns into a slideshow. Stage three, a build fails on a machine that is not yours because the Google Maps API key was only ever in one place. This guide is the version of react-native-maps I wish someone had handed me before stage two.
Versions here are pinned to react-native-maps 1.29.8, published September 20, 2026. The library is MIT, sits at roughly 16,000 GitHub stars, and pulls around 890,000 downloads a week, which makes it the default map component for React Native by a wide margin.
What react-native-maps actually is
It is a thin React wrapper over two completely different native SDKs. On Android you get Google Maps. On iOS you choose between Apple MapKit, which is built into the OS and needs no key, and the Google Maps SDK, which needs a key and a pod. One JavaScript API, two rendering engines, and a set of behavioural differences you will meet on your first Android bug report.
That framing matters because most confusing react-native-maps issues are not library bugs. They are places where MapKit and Google Maps genuinely behave differently and the wrapper does not pretend otherwise.
One thing to check before you install
The npm beta dist-tag on this package points at 2.0.0-beta.15, published in May 2024. It has not moved since. Do not install it because the number looks newer. The maintained line is 1.x, and as of this week it is moving fast: 1.29.3 through 1.29.8 all shipped between September 19 and 20.
Installation
Expo
If you are on Expo SDK 53 or newer, this is a config plugin and nothing else. The plugin needs react-native-maps 1.22 or above.
{
"expo": {
"plugins": [
[
"react-native-maps",
{
"iosGoogleMapsApiKey": "YOUR_KEY_HERE",
"androidGoogleMapsApiKey": "YOUR_KEY_HERE"
}
]
]
}
}
Drop the key options entirely if you are using Apple Maps on iOS and only need the Android key. Then rebuild the dev client, because a config plugin does not apply to an already built binary. If you are new to how that works, our Expo guide covers config plugins and prebuild.
Bare React Native, iOS
Apple Maps works after npx pod-install with no key at all. That is the fastest route to a rendering map and it is a perfectly good production choice for a lot of apps.
For Google Maps on iOS you add the pod and call provideAPIKey as the first statement in your app delegate. Projects on RN 0.77 and above ship a Swift delegate:
import GoogleMaps
@main
class AppDelegate: RCTAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
GMSServices.provideAPIKey("YOUR_KEY_HERE")
// ...
}
}
In the Podfile, above use_native_modules!:
rn_maps_path = '../node_modules/react-native-maps'
pod 'react-native-maps/Google', :path => rn_maps_path
Google Maps SDK for iOS requires iOS 14, so set your deployment target and your Podfile platform to 14 or higher.
Bare React Native, Android
Android always needs a Google Maps key, in the manifest, and a Google Cloud billing account attached to the project. The most common failure mode here is not a crash, it is a map that renders as a grey grid with the Google logo in the corner. That is almost always an unrestricted or wrong key, or billing not enabled. Check logcat, the SDK says so explicitly.
The core API
import MapView, {Marker, PROVIDER_GOOGLE} from 'react-native-maps';
export function StoreMap() {
return (
<MapView
style={{flex: 1}}
provider={PROVIDER_GOOGLE}
initialRegion={{
latitude: 51.5072,
longitude: -0.1276,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
}}>
<Marker
coordinate={{latitude: 51.5072, longitude: -0.1276}}
title="Head office"
description="Open until 18:00"
/>
</MapView>
);
}
Two notes on that snippet. style={{flex: 1}} is not optional decoration, a MapView with no height renders nothing and gives you no error. And provider only means anything on iOS: on Android it is Google Maps regardless.
region versus initialRegion, the trap everyone hits
initialRegion is uncontrolled. You set it once, the user pans, you stay out of the way. region is controlled, which means every render pushes the region back into the native view.
The classic bug: you store the region in state, update it from onRegionChangeComplete, and pass it back through region. Now every pan triggers a state update that pushes a region back to the map mid gesture, and the map fights the user’s finger. It shows up as a map that snaps back or judders while dragging.
The rule I use: default to initialRegion, keep the current region in a ref rather than state if you need to read it, and move the camera imperatively when you actually want to move it.
const mapRef = useRef<MapView>(null);
mapRef.current?.animateCamera(
{center: {latitude: 51.5072, longitude: -0.1276}, zoom: 14},
{duration: 400},
);
Same idea in camera form: initialCamera and camera, with getCamera, setCamera and animateCamera on the ref. Cameras express heading and pitch, regions express a bounding box. Use whichever matches the thing you are trying to say.
Fitting the map to your data
Three imperative methods do most of the work here and they save a lot of arithmetic:
mapRef.current?.fitToCoordinates(coords, {
edgePadding: {top: 64, right: 32, bottom: 160, left: 32},
animated: true,
});
fitToCoordinates takes an array of points, fitToSuppliedMarkers takes marker identifiers, and fitToElements takes everything on the map. Call them after onMapReady, not in an effect that runs on mount, because on Android the native map is frequently not ready yet and the call is silently dropped. That single ordering mistake accounts for a large share of “fitToCoordinates does nothing” issues.
Generous edgePadding at the bottom is what stops your bottom sheet covering half the pins. If you are building that layout, our modals and bottom sheets guide covers the sheet half.
Markers, and why they get slow
A plain Marker with a pinColor or an image is cheap. A Marker with React children is not, because the native layer has to rasterise your view into a bitmap every time it thinks the content changed.
<Marker coordinate={item.coordinate} tracksViewChanges={false}>
<PriceBubble price={item.price} />
</Marker>
tracksViewChanges defaults to true. Leave it on with a few dozen custom markers and you get visible jank on every pan. Turn it off and your markers stop updating when their content changes, which is the correct trade almost always. The pattern that works: mount with tracksViewChanges true, flip it to false once the content has painted, and flip it back briefly when the data genuinely changes.
The other marker props worth knowing: anchor and centerOffset to control which part of your view sits on the coordinate, flat to keep a marker pinned to the map plane rather than the screen as the camera rotates, tracksInfoWindowChanges for the Android callout equivalent of the problem above, identifier so fitToSuppliedMarkers can find it, and opacity, rotation and draggable for the obvious things.
This week’s 1.29.5 fixed custom marker views getting clipped on Android by an undersized bitmap, and 1.29.8 added flat to the native marker component. If either of those sounds like a bug you filed, upgrade.
Past a few hundred markers, cluster
There is no clustering in the box. The honest options are to cluster in JavaScript before you render, using supercluster directly or a wrapper such as react-native-map-clustering, or to move to a vector renderer that clusters natively. Whatever you pick, cluster by the current zoom level and render clusters as plain image markers rather than custom views, so you keep the tracksViewChanges win.
Everything else on the canvas
The package exports more than most people use: Polyline, Polygon, Circle, Overlay for an image pinned to a bounding box, Heatmap, Geojson for rendering a FeatureCollection straight from an API, Callout and CalloutSubview, and three tile components: UrlTile for an XYZ server, WMSTile, and LocalTile for tiles shipped or downloaded onto the device.
LocalTile is the underrated one. If your app has an offline mode and a fixed area of interest, bundling tiles is far simpler than any offline SDK feature and it works identically on both platforms.
Geojson is the fastest path from a backend that already speaks GeoJSON to something on screen, but it renders every feature as a child component, so it is not a route around the marker count problem.
Performance checklist
- Set
tracksViewChanges={false}on custom markers. Single biggest win, every time. - Do not put the region in state and feed it back through
region. UseinitialRegionplus imperative camera moves. - Memoise your marker list. A parent re-render that rebuilds a 300 element array of new objects will re-render 300 markers, even if nothing moved.
- Debounce work hanging off
onRegionChange. It fires continuously during a drag. Do network work inonRegionChangeComplete. - Consider
liteModeon Android for a map in a list row or a card. It renders a static bitmap instead of a live map and costs a fraction as much. - Use
imageoriconinstead of children wherever a pin is genuinely just an icon.
New Architecture status
Fabric support arrived incrementally rather than in one release, and 1.29.0 in June added iOS Fabric support for the Google Maps Marker and Polygon. Current peer ranges are react >= 18.3.1, react-native >= 0.76.0, and react-native-web >= 0.11. In practice: this library works on the New Architecture, but it is the part of the stack most likely to surface a platform specific layout or event quirk, so upgrade it as its own change rather than inside a five library bump. That advice applies broadly, and we made the same argument in the react-native-screens guide.
When to use something else
react-native-maps is the right default. It is not always the right answer.
@rnmapbox/maps(10.3.5, roughly 167,000 downloads a week). Vector tiles, real offline packs, native clustering, deep styling. The cost is a Mapbox account, a usage-based bill, and a heavier install. If maps are the product rather than a screen in the product, start here.@maplibre/maplibre-react-native(11.4.0, shipped September 19). The open source fork of the Mapbox GL lineage. Same vector rendering model with no vendor tie, as long as you are willing to source or host tiles.expo-maps(57.0.3). Expo’s own module, Google Maps on Android and Apple Maps on iOS, much smaller API surface. Worth a look for a simple map in an Expo app, but check its documented limitations before you commit, because it is a newer and deliberately narrower library.
Three bugs you will probably hit
Grey map with a Google logo. Not a rendering bug. API key missing, restricted to the wrong package name or bundle ID, or billing not enabled on the Cloud project. Android and iOS need separate keys.
The map is invisible. No explicit height. flex: 1 on the MapView and on its parent, or a fixed height. There is no warning for this.
Markers drift when you rotate or zoom. Wrong anchor for a custom marker view, or a transform on the marker content. There was a genuine iOS bug in that area fixed in 1.29.2 on September 13, so upgrade before you start debugging your own anchors.
FAQ
Do I need a Google Maps API key for iOS? Only if you set provider={PROVIDER_GOOGLE}. Apple Maps is the default on iOS and needs no key.
Does react-native-maps work with Expo Go? Use a development build. The config plugin changes native project files, and Expo Go ships a fixed native runtime.
Can I show a map on web? There is a react-native-web peer, but treat web as best effort. If web is a first class target, render a web map library there behind a platform split.
How many markers is too many? With tracksViewChanges={false} and image markers, several hundred is fine on mid range hardware. With custom view markers, the number where it gets uncomfortable is closer to fifty. Cluster above that.
Is the 2.0 beta usable? No. The beta tag has pointed at a May 2024 build for well over a year. Stay on 1.x.