expo-maps Tutorial 2026: Native Apple Maps and Google Maps for React Native
Ship native Apple Maps and Google Maps in a React Native app with expo-maps: install the config plugin, render your first map, drop markers, handle camera events, and decide when to stay on react-native-maps.
expo-maps is the Expo team's native maps library for React Native, built directly on SwiftUI's Map on iOS and Jetpack Compose's Google Maps composable on Android. You install it with npx expo install expo-maps, render <AppleMaps.View /> on iOS or <GoogleMaps.View /> on Android, and drop markers, polylines, and camera controls without touching Xcode or Gradle. As of SDK 57 it's still officially in alpha, requires a development build (no Expo Go), and needs iOS 18 as the deployment target for the full event surface.
expo-maps exposes two platform-specific components (AppleMaps.View for iOS/SwiftUI MapKit and GoogleMaps.View for Android/Compose Google Maps) instead of one cross-platform MapView.
It ships as an alpha library in SDK 53+ (57 at time of writing), is not available in Expo Go, and requires a development build via EAS Build or npx expo run:ios | run:android.
Google Maps on iOS is intentionally not supported. If you need Google Maps everywhere, stay on react-native-maps.
Configuration is entirely declarative: the API key for Android sits in app.json under android.config.googleMaps.apiKey, and the location permission prompt is set through the expo-maps config plugin.
The library supports markers, iOS annotations, polylines, polygons, circles, camera control, and event handlers (onMapClick, onMarkerClick, onCameraMove, onPOIClick), but it does not render arbitrary React components as marker content.
For greenfield Expo apps targeting iOS 18+, expo-maps gives you a faster startup and truer platform look-and-feel. For existing production apps, react-native-maps is still the safer choice.
What is expo-maps and how is it different from react-native-maps?
Coming from React web, my mental model for a map component used to be a single <Map /> that hides everything platform-specific. That's how react-native-maps works: one MapView, with props like provider="google" to switch renderers. expo-maps deliberately breaks that model. It exposes two components (AppleMaps.View and GoogleMaps.View), and each one is a thin JavaScript wrapper around the actual native declarative UI on that platform. On iOS that means SwiftUI's Map from MapKit. On Android it means the GoogleMap composable from maps-compose.
The upside of that choice is that map interactions feel genuinely native. Panning, tilting, 3D buildings, POI callouts, and the compass all match what the OS ships in Apple Maps and Google Maps proper, because the underlying view is the OS view. It isn't a rewritten bridge on top of MKMapView. Startup is also faster: SwiftUI initializes lazily and the Compose side avoids the older bridging cost that react-native-maps historically paid with the paper renderer.
The trade-off is scope. Because SwiftUI's Map API is younger than MapKit's UIKit view, and because Compose Maps is a separate SDK from the Google Maps SDK for iOS, expo-maps only exposes what both platforms genuinely agree on. Google Maps on iOS is out. React components as marker content, a very common react-native-maps pattern, is out. Heatmaps, custom tile providers, and clustering plugins are out until the underlying native APIs cover them. If your app relies on that ecosystem, the community package remains the pragmatic choice, as I covered in the react-native-maps guide with markers, clustering, and directions.
Install expo-maps in an Expo project
You need a project on Expo SDK 53 or newer (57 is current). Because expo-maps ships native code that isn't baked into the Expo Go binary, you also need a development build produced by a config plugin. Nothing about that is unusual for 2026 (most useful native libraries need one now), but if you've been prototyping in Expo Go, this is where you graduate.
Install the package with the Expo CLI so the correct version is pinned to your SDK:
npx expo install expo-maps
Add the plugin to your app.json (or app.config.ts) so prebuild wires up the iOS Info.plist entries and Android manifest permissions. The plugin's job is to declare the location permission prompt string and to opt you into requesting while-using-app location access:
{
"expo": {
"name": "map-demo",
"slug": "map-demo",
"ios": {
"bundleIdentifier": "com.example.mapdemo",
"deploymentTarget": "18.0"
},
"android": {
"package": "com.example.mapdemo"
},
"plugins": [
[
"expo-maps",
{
"requestLocationPermission": true,
"locationPermission": "Allow $(PRODUCT_NAME) to show your location on the map."
}
]
]
}
}
Regenerate native code and run the app. From this point on you always use the development build, not Expo Go:
Apple Maps needs zero credentials, since MapKit is included in every iOS app. Google Maps for Android needs a key from a Google Cloud project with the Maps SDK for Android API enabled. Once you have the key, put it in app.json under android.config.googleMaps.apiKey and Expo's prebuild will inject the correct meta-data entry into AndroidManifest.xml. Don't commit a raw key into the file if the repo is public. Pull it from an environment variable via app.config.ts instead:
// app.config.ts
import type { ExpoConfig } from 'expo/config';
export default (): ExpoConfig => ({
name: 'map-demo',
slug: 'map-demo',
ios: {
bundleIdentifier: 'com.example.mapdemo',
deploymentTarget: '18.0',
},
android: {
package: 'com.example.mapdemo',
config: {
googleMaps: {
apiKey: process.env.GOOGLE_MAPS_ANDROID_KEY,
},
},
},
plugins: [
[
'expo-maps',
{
requestLocationPermission: true,
locationPermission: 'Allow $(PRODUCT_NAME) to show your location on the map.',
},
],
],
});
In EAS, set the value once per environment with eas env:create --scope project --name GOOGLE_MAPS_ANDROID_KEY --value "...". The key is only needed at build time. Android reads it from the manifest, so there's no runtime call to make. Restrict the key in Google Cloud Console to your Android package name and SHA-1 signing fingerprint. Both debug and release fingerprints belong on the whitelist. Google's Android SDK setup docs spell out how to fetch fingerprints from Gradle.
Render your first map on iOS and Android
The two views are similar enough that a small platform switch covers most screens. Here's the smallest useful example: a map centered on San Francisco with default UI controls visible.
A few things to notice. cameraPosition takes a plain object with coordinates and zoom. No latitudeDelta/longitudeDelta like react-native-maps. Zoom levels are Google-style integers (roughly 0 = world, 20 = building). properties controls what the map renders (traffic, buildings, indoor floors, map type), and uiSettings controls the on-screen chrome (compass, scale bar, my-location button). Splitting the two mirrors how SwiftUI and Compose expose the same distinction natively, and it means you rarely re-render the whole map just to toggle a control.
Add markers and annotations
Markers are the workhorse of any map screen. On both platforms, pass an array to the markers prop, and expo-maps will diff the array between renders for you. Wrap the array in useMemo so identity is stable and the native view doesn't needlessly reconcile:
On iOS you also get the richer AppleMaps.Marker configuration. systemImage lets you use any SF Symbol as the pin icon, monogram renders a two-letter badge, and tintColor replaces the classic red drop with a brand color. Honestly, that last one is what I reach for whenever I need consistent marker styling across a design system.
Annotations are an iOS-only concept exposed through the annotations prop on AppleMaps.View. Unlike markers, annotations render a custom bitmap (fetched via expo-image) instead of the built-in pin. This is how you get logo-shaped pins without shipping the image as an app asset:
Camera movement is expressed declaratively. You update the cameraPosition prop, and the native view animates to the new position. There's also an imperative ref API for one-off transitions like "fit to markers" when you don't want a re-render to trigger the flight:
onMapClick(event): fires with { coordinates } when the user taps empty map.
onMarkerClick(event): fires with the marker payload. iOS 18+ only on AppleMaps.View, and always available on GoogleMaps.View.
onCameraMove(event): fires while the camera is moving. Payload includes coordinates, zoom, bearing, and tilt. Debounce this if you use it for network fetches.
onPOIClick(event): Android only. Useful when you want to intercept Google-provided POIs (restaurants, parks) and show your own detail sheet.
onMapLongClick(event): Android only. The idiom for "drop a pin where I long-pressed".
Draw polylines, polygons, and circles
Overlays are also declarative arrays. This example draws a walking route on iOS with a rounded, dashed style:
Polygons follow the same shape as polylines but with a filled interior. On GoogleMaps.View, both accept an onClick lambda so you can build interactive overlays without wrapping each shape in a listener. This is where the SwiftUI/Compose foundation really pays off, because the overlay API on both platforms is genuinely rich, and expo-maps is a thin pass-through.
Should you migrate from react-native-maps to expo-maps?
So, here's my honest take after moving two side projects and shelving the migration for a client app. expo-maps is the right choice for new Expo projects that only need core map functionality and can live with two platform-specific components. It's the wrong choice for existing apps with a complex overlay ecosystem, and it's the wrong choice when your product mandates Google Maps tiles on iOS.
Dimension
expo-maps (2026)
react-native-maps (2026)
Renderer
SwiftUI MapKit + Compose Google Maps
MKMapView + Google Maps SDK
iOS provider options
Apple Maps only
Apple Maps or Google Maps
Expo Go
Not supported
Not supported (dev build needed)
Custom React marker content
No
Yes
Clustering plugins
None yet
Rich third-party ecosystem
New Architecture
Yes, native
Yes, since 1.14
Minimum iOS
18 for full events
13
Stability
Alpha, breaking changes each SDK
Stable, semver-respecting
If you're shipping a store locator, a run tracker, or any app whose map is essentially "show pins on a map and let me tap them", expo-maps genuinely delivers a nicer UX with less code. If you're shipping a delivery app with heatmaps, custom tile servers, or dense clustering, the ecosystem around react-native-maps is still where the work has been done, and pairing it with the React Native New Architecture migration checklist gives you the same underlying performance model.
For deeper background on why the Expo team built their own maps library on top of SwiftUI and Compose rather than wrapping the existing SDKs, the Introducing expo-maps blog post lays out the reasoning and the roadmap.
Common errors and how to fix them
A few footguns I've hit or seen on the Expo Discord in the past few weeks:
"Native module cannot be null" on launch. You're running in Expo Go. Rebuild the dev client with npx expo run:ios or eas build --profile development.
Blank grey map on Android. The Google Maps API key is missing, restricted to the wrong package name, or the Maps SDK for Android isn't enabled on the Google Cloud project. Check adb logcat | grep -i "Google Maps"; the SDK logs the exact cause.
Marker taps do nothing on iOS. You're below iOS 18. Set ios.deploymentTarget to "18.0" in app.json, re-run npx expo prebuild --clean, and rebuild.
Camera jumps back to the initial position on state change. You're passing a fresh object literal to cameraPosition on every render, which the native side interprets as "please animate back". Memoize the object, or use the imperative ref API.
Location dot missing. The plugin's requestLocationPermission: true only declares the string; you still need to call Location.requestForegroundPermissionsAsync() from expo-location at runtime before the map's my-location layer will populate.
You can browse the full prop reference and TypeScript types in the official expo-maps documentation, and pin exact versions from the expo-maps npm page if you need to lock behavior while the API stabilizes.
Frequently Asked Questions
Does expo-maps work with Expo Go?
No. expo-maps ships its own native code and isn't baked into the Expo Go binary. You need a development build produced with npx expo run:ios, npx expo run:android, or an EAS Build development profile. Once the dev client is installed, hot reload works exactly like Expo Go.
Can I use Google Maps on iOS with expo-maps?
No. expo-maps intentionally supports only Apple Maps on iOS and only Google Maps on Android. If your product mandates Google Maps tiles on both platforms, use react-native-maps instead, since it wraps the Google Maps SDK for iOS and lets you pass provider="google".
Is expo-maps production ready in 2026?
Not officially. The package is still marked alpha in SDK 57 and the maintainers explicitly warn about frequent breaking changes. Several teams already run it in production for simple map screens, but you should pin the version, subscribe to the changelog, and budget a small migration cost each SDK upgrade until it stabilizes.
How do I add a marker to expo-maps?
Pass an array of marker objects to the markers prop on either AppleMaps.View or GoogleMaps.View. Each object needs coordinates: { latitude, longitude }, and optionally title, snippet, tintColor, systemImage (iOS), or a custom icon. Memoize the array so the native side doesn't diff on every render.
What is the difference between markers and annotations in expo-maps?
Both drop a graphic at a coordinate. Markers use the platform's built-in pin appearance and support light styling (tint color, SF Symbol, monogram). Annotations are iOS-only and let you supply a fully custom image, typically loaded with expo-image's useImage, so you can render branded pins without shipping the image as an app asset.
Do I need an API key for Apple Maps in expo-maps?
No. MapKit is bundled with every iOS app and doesn't require registration or credentials for on-device rendering. You only need a Google Cloud API key with the Maps SDK for Android enabled, which you set through android.config.googleMaps.apiKey in app.json or app.config.ts.
Capture heap snapshots from a Hermes-powered React Native app in Expo, read retainer paths in Chrome DevTools, and catch the three most common leaks with numbers, traces, and CI regression tests.
Ship iOS home screen widgets from an Expo React Native app using @bacons/apple-targets. Covers App Groups, SwiftUI layouts, App Intents on iOS 17+, deep-linking, and the debugging traps that eat weekends.