☕ How I Actually Got Google Maps Working in React Native (and Why It Took Me Way Longer Than It Should Have)
So last week, I'm building this Coffee Shop Locator app. Simple idea, right? Show users nearby cafés on a map.
I figured I'd just drop in a <MapView> component, maybe add a few markers, and boom—done by lunch.
Yeah... that didn't happen. 😅
Three hours later, I'm staring at a blank gray screen, Googling "why is my react native map not showing" for the hundredth time, and questioning all my life choices.
But I finally got it working. And honestly? Once I understood what was actually happening under the hood, it all made sense.
So here's everything I learned—written for past me, and hopefully helpful for you too.
🧩 Why Maps Aren't Just "Install and Go"
Here's the thing nobody tells you upfront: React Native doesn't come with maps built-in.
When you think "map in my app," you're probably picturing something like Google Maps just... working. But that's not quite how it goes.
Under the hood, maps work completely differently on each platform:
- Android uses the Google Maps SDK (native Java/Kotlin stuff)
- iOS uses Apple's MapKit (native Swift code)
Your React Native app? That's all JavaScript. The map itself lives in native land.
So you need something that bridges the gap—something that lets you write simple JavaScript like this:
<MapView>
<Marker coordinate={{ latitude: 28.61, longitude: 77.23 }} />
</MapView>
...and then magically translates that into the actual native code that each platform understands.
That bridge is react-native-maps. And honestly, once I understood this, a lot of the setup headaches made more sense.
🌍 What react-native-maps Actually Does
Think of it like a translator at a conference.
You (JavaScript developer) speak one language. The native SDKs (Google Maps, Apple Maps) speak another. react-native-maps sits in the middle and makes sure everyone understands each other.
Without it, you'd be writing separate native modules in Kotlin and Swift yourself. No thanks. 😬
⚙️ The Part Where I Actually Set This Up (Android Edition)
Alright, this is where most tutorials lose me with vague instructions. So I'm gonna walk through exactly what I did, step by step.
🧭 Step 1: Get Your Google Maps API Key
Head over to the Google Cloud Console.
Click "Create API Key" and make sure you enable these three things:
- ✅ Maps SDK for Android
- ✅ Maps SDK for iOS
- ✅ Places API (if you want search/autocomplete later)
Copy that API key. You'll need it in a second.
🔑 Step 2: Tell Android About Your API Key
Open up your Android manifest file:
android/app/src/main/AndroidManifest.xml
Inside the <application> tag, paste this:
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_GOOGLE_MAPS_API_KEY"/>
This is basically how your Android app knows which API key to use when it talks to Google's servers.
⚠️ If you're using Expo and run into issues:
Sometimes Expo gets weird when you manually edit Android files. If your map still won't show up, or Expo throws errors during build, try adding the API key to your app.json instead:
"android": {
"package": "com.example.coffeemap",
"config": {
"googleMaps": {
"apiKey": "${GOOGLE_MAPS_API_KEY}"
}
}
}
I ran into this myself—spent way too long trying to figure out why the manifest approach wasn't working. Turns out Expo sometimes overrides those manual changes during build. Using app.json lets Expo handle it properly.
📝 Pro tip: Don't leave your API key wide open. Go restrict it in the Cloud Console—tie it to your app's package name and SHA-1 fingerprint. (I'll show you how to get those in the next step.)
🔍 Step 3: Get Your SHA-1 Fingerprint
Okay, this part confused me at first. What even is a SHA-1 fingerprint?
Basically, it's a unique digital signature for your app. It comes from the keystore file that signs your APK. When you register your app with Google, they use this fingerprint to verify "yep, this request is really coming from YOUR app."
Each keystore = different SHA-1 = different identity.
Here's how to get it:
📂 Navigate to your Android folder
cd android
💻 Run the signing report
On Windows (Command Prompt or PowerShell):
gradlew signingReport
On macOS or Linux:
./gradlew signingReport
You'll see a bunch of output. Look for something like this:
Variant: debug
SHA1: 12:34:56:AB:CD:EF:98:76:54:32:10:FF:EE:DD:CC:BB:AA:99:88
Package name: com.example.coffeemap
Copy both the SHA-1 and your package name (also called applicationId).
Now go back to Google Cloud Console → Credentials → click on your API key → "Restrict Key" → Android Apps.
Add your package name and SHA-1 there.
⚠️ Important: Make sure you're using the right SHA-1 for your build type! There's one for debug (what you use when testing locally) and one for release (what you use for the Play Store). Use the debug one while you're developing.
🧹 Step 4: Clean Everything and Rebuild
Sometimes Android gets stubborn and caches old stuff. So do this:
cd android
./gradlew clean
cd ..
npx react-native run-android
If the map is still blank (which happened to me), uninstall the app completely from your device/emulator and reinstall it. That finally did the trick for me.
🗺️ The Moment of Truth: A Working Map 🎉
Okay, after all that setup... here's the payoff. This is the simplest version that actually works:
import React from "react";
import { View, StyleSheet } from "react-native";
import MapView, { Marker } from "react-native-maps";
export default function CoffeeMap() {
return (
<View style={styles.container}>
<MapView
provider="google"
style={styles.map}
initialRegion={{
latitude: 28.6139, // New Delhi
longitude: 77.2090,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
}}
onMapReady={() => console.log("✅ Map is ready!")}
>
<Marker
coordinate={{ latitude: 28.6139, longitude: 77.2090 }}
title="Central Café"
description="Your daily caffeine fix ☕"
/>
</MapView>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
});
When I finally saw that map load with my little red pin on it... honestly, such a satisfying moment. 😊
🧠 Understanding What These Components Actually Do
Once you have a working map, you'll probably want to do more with it. Here's a breakdown of the main components I used:
| Component | What It Does | Example |
|---|---|---|
<MapView> |
The actual map | Base canvas for everything |
<Marker> |
A pin on the map | Mark café locations |
<Callout> |
Info popup when you tap a marker | Show details, ratings, hours |
<Polyline> |
Draw lines on the map | Show routes or paths |
<Circle> |
Highlight a circular area | Show delivery radius, etc. |
Let me break down each one a bit more...
🗺️ <MapView> — Your Map Canvas
<MapView
provider="google"
style={{ flex: 1 }}
initialRegion={{
latitude: 37.7749,
longitude: -122.4194,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
}}
showsUserLocation={true}
onMapReady={() => console.log("Map loaded!")}
/>
Quick tip: The latitudeDelta and longitudeDelta control your zoom level. Smaller numbers = more zoomed in. I usually just play with these values until it looks right.
📍 <Marker> — The Pin
<Marker
coordinate={{ latitude: 37.7749, longitude: -122.4194 }}
title="Best Bites Restaurant"
description="Open till 11 PM!"
pinColor="#FF5733"
/>
You can also use your own custom image instead of the default red pin:
image={require('../assets/restaurant-pin.png')}
I did this for my coffee shops—added little coffee cup icons. Makes it feel more polished.
💬 <Callout> — Info Popup
<Marker coordinate={{ latitude: 37.7749, longitude: -122.4194 }}>
<Callout>
<View style={{ width: 200, padding: 10 }}>
<Text style={{ fontWeight: "bold" }}>Best Bites</Text>
<Text>⭐ 4.5 • Fast Delivery</Text>
</View>
</Callout>
</Marker>
This is where you can get creative—show ratings, photos, business hours, whatever makes sense for your app.
🛣️ <Polyline> — Draw Routes
<Polyline
coordinates={[
{ latitude: 37.7749, longitude: -122.4194 },
{ latitude: 37.7849, longitude: -122.4094 },
]}
strokeColor="#007AFF"
strokeWidth={4}
/>
Perfect for showing delivery routes, walking paths, driving directions—anything that connects point A to point B.
🔵 <Circle> — Highlight Areas
<Circle
center={{ latitude: 37.7749, longitude: -122.4194 }}
radius={3000} // 3 km radius
strokeColor="#FF0000"
fillColor="rgba(255,0,0,0.2)"
/>
I used this to show the delivery range for each café. Users could see at a glance if they're within range.
Perfect ✅ — here’s your updated, human-sounding, emotional, and storytelling-style section rewritten so it fits the Coffee Shop context (instead of the clinic locator).
Everything sounds natural and still feels developer-friendly 👇
🚀 Going Beyond the Basics: Making It Interactive
Here’s where it got fun for me. Just showing a static map felt... boring.
I wanted users to actually interact with it — move things around, see live updates, and feel like they’re really controlling where their coffee is coming from ☕.
So instead of a simple static café map, I built an interactive Coffee Shop Locator — where users can:
✅ Use their current GPS location
✅ Drag the coffee pin to pick their favorite café spot
✅ See live latitude and longitude updates
✅ Watch the map respond in real-time as they explore
Here’s the full code for that magic 👇
import React, { useEffect, useState } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from "react-native";
import MapView, { Marker } from "react-native-maps";
import * as Location from "expo-location";
import Constants from "expo-constants";
import { scale, verticalScale } from "react-native-size-matters";
export default function CoffeeShopMapSelectionScreen() {
const [region, setRegion] = useState({
latitude: 20.5937,
longitude: 78.9629,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
});
const [loadingLocation, setLoadingLocation] = useState(false);
const [hasPermission, setHasPermission] = useState(false);
const googleApiKey = Constants.expoConfig?.extra?.GOOGLE_MAPS_API_KEY;
console.log("Using API key:", googleApiKey);
const requestLocationPermission = async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
alert("Permission to access location was denied.");
return;
}
setHasPermission(true);
};
const fetchCurrentLocation = async () => {
try {
setLoadingLocation(true);
const { coords } = await Location.getCurrentPositionAsync({});
setRegion((prev) => ({
...prev,
latitude: coords.latitude,
longitude: coords.longitude,
}));
} catch (error) {
console.log("Error fetching current location:", error);
} finally {
setLoadingLocation(false);
}
};
useEffect(() => {
requestLocationPermission();
}, []);
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>☕ Pick Your Coffee Spot</Text>
<TouchableOpacity
onPress={fetchCurrentLocation}
style={styles.currentLocationBtn}
>
{loadingLocation ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.currentLocationText}>Use Current Location</Text>
)}
</TouchableOpacity>
</View>
<MapView
style={styles.map}
provider="google"
initialRegion={region}
onRegionChangeComplete={setRegion}
showsUserLocation={true}
onMapReady={() => console.log("✅ Map ready")}
>
<Marker
coordinate={{
latitude: region.latitude,
longitude: region.longitude,
}}
draggable
onDragEnd={(e) => {
const { latitude, longitude } = e.nativeEvent.coordinate;
setRegion({ ...region, latitude, longitude });
}}
pinColor="#b76e79"
title="Your Coffee Spot"
description="Drag to adjust location"
/>
</MapView>
<View style={styles.locationDetails}>
<Text style={styles.coordText}>
🌍 Latitude: {region.latitude.toFixed(6)}
</Text>
<Text style={styles.coordText}>
📍 Longitude: {region.longitude.toFixed(6)}
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#fff" },
map: { flex: 1 },
header: {
paddingHorizontal: scale(16),
paddingVertical: verticalScale(8),
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
headerTitle: {
fontSize: 16,
fontWeight: "600",
color: "#222",
},
currentLocationBtn: {
backgroundColor: "#8B4513",
paddingHorizontal: scale(12),
paddingVertical: verticalScale(6),
borderRadius: 10,
},
currentLocationText: {
color: "#fff",
fontSize: 14,
fontWeight: "500",
},
locationDetails: {
backgroundColor: "#F9F9F9",
paddingVertical: verticalScale(10),
paddingHorizontal: scale(16),
borderTopWidth: 1,
borderColor: "#EAEAEA",
},
coordText: {
fontSize: 14,
color: "#333",
marginBottom: 4,
},
});
💡 What’s Happening Behind the Scenes
-
Permission Request: The app politely asks for location access using
expo-location. - GPS Centering: One tap centers the map right on your current location.
- Draggable Pin: Users can drag the pin to choose exactly where their favorite café is.
- Live Updates: Latitude and longitude update in real time below the map.
Now, it doesn’t just show a map — it feels alive ☕✨
🎨 Making Your Map Look Less... Generic
One thing I realized pretty quickly—the default Google Maps style is fine, but it doesn't really match every app's vibe.
You can completely customize how your map looks using JSON style files. Dark mode, minimalist, retro, whatever fits your design.
Create a file called map-style.json:
[
{
"featureType": "all",
"elementType": "labels",
"stylers": [{ "visibility": "off" }]
}
]
Then apply it:
import mapStyle from '../assets/map-style.json';
<MapView
customMapStyle={mapStyle}
style={{ flex: 1 }}
/>
Pro tip: Use the Google Map Styling Wizard to visually design your style. Way easier than hand-writing JSON. You just click around, pick colors, toggle elements on and off, then download the JSON file when you're done.
I made mine slightly darker with muted colors so it didn't compete with my UI elements. Made a huge difference in how professional the app felt.
📍
From coordinates to readable addresses — reverse geocoding made simple
So you've got your map working in React Native. Users can pan around, see their location, maybe drop some markers. That's cool.
But here's what I realized when testing my coffee shop locator—when someone moves the map around, they don't just want to see latitude: 20.5937, longitude: 78.9629. They want to know: "What place am I actually looking at?"
That's where reverse geocoding comes in—converting coordinates back into human-readable addresses.
And honestly? Getting this to work smoothly taught me way more about maps than I expected.
Let me show you what I learned.
⚠️ Quick Fix: If Your Geocoding API Isn't Working
Before we dive in—if you're getting API errors even with the correct SHA-1 configured, here's what saved me hours of debugging:
Go to Google Cloud Console → Credentials → Your API Key → Application restrictions
If you're still in development and things aren't working, temporarily set it to "None".
Sometimes the SHA-1 + package name restrictions get finicky during development (especially with debug vs release builds). Once everything's working, you can lock it down again for security.
🎯 The Fixed Pin Approach (Better UX)
First, a quick design choice that made a huge difference.
Instead of having users drag a marker around (which can feel clunky), I removed the draggable <Marker> completely and added a fixed pin overlay that stays centered on the screen.
The map moves underneath it. Way more intuitive.
Here's the code:
{/* Map Container */}
<View style={styles.mapContainer}>
<MapView
style={styles.map}
provider="google"
initialRegion={region}
onRegionChangeComplete={handleRegionChangeComplete}
showsUserLocation={true}
mapType="standard"
onMapReady={() => console.log("✅ Map ready")}
/>
{/* FIXED PIN OVERLAY - stays in center */}
<View style={styles.fixedPinContainer}>
<View style={styles.pinCircle}>
<View style={styles.pinInnerCircle} />
</View>
</View>
</View>
What's happening here?
The <MapView> fills the container, and the pin is positioned absolutely in the center using styles. As users pan the map, the pin never moves—it's always pointing at the center coordinates.
🔄 Reverse Geocoding: Two Ways to Do It
Now for the actual geocoding part.
Every time the user finishes panning, onRegionChangeComplete fires with the new center coordinates. We use those coordinates to fetch the address.
I discovered there are two approaches, and they each have trade-offs.
📱 Method 1: Using Expo Location (Simple & Clean)
import * as Location from "expo-location";
const handleRegionChangeComplete = async (newRegion: any) => {
setRegion(newRegion);
try {
const [place] = await Location.reverseGeocodeAsync({
latitude: newRegion.latitude,
longitude: newRegion.longitude,
});
console.log("📍 Place from Expo:", place);
if (place) {
setAddress(
`${place.name || ""} ${place.street || ""}, ${place.city || ""}, ${place.region || ""}`
);
}
} catch (error) {
console.log("Error fetching place:", error);
}
};
What you get back:
{
"city": "Wadgaon",
"country": "India",
"district": null,
"formattedAddress": "144, Khattalwada, Wadgaon, Maharashtra 442301, India",
"isoCountryCode": "IN",
"name": "144",
"postalCode": "442301",
"region": "Maharashtra",
"street": null,
"streetNumber": null,
"subregion": "Nagpur Division",
"timezone": null
}
✅ Pros:
- Super simple—one function call
- Already included with Expo (no extra setup)
- Clean, structured response
- Easy to parse
❌ Cons:
- Less detailed—sometimes missing street names or landmarks
- No control over result quality
- Limited to basic address components
When to use it: If you just need "City, State" or a simple formatted address, this is perfect. No fuss, no API keys to manage.
🌐 Method 2: Using Google Geocoding API (Detailed & Powerful)
const handleRegionChangeComplete = async (newRegion: any) => {
setRegion(newRegion);
try {
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${newRegion.latitude},${newRegion.longitude}&key=${googleApiKey}`
);
const data = await response.json();
const bestResult = data.results[0];
console.log("📍 Best result from Google:", bestResult);
// Use formatted_address or parse address_components
setAddress(bestResult.formatted_address);
} catch (error) {
console.log("Error fetching place:", error);
}
};
What you get back:
{
"address_components": [
{"long_name": "144", "short_name": "144", "types": ["street_number"]},
{"long_name": "Khattalwada", "short_name": "Khattalwada", "types": ["sublocality"]},
{"long_name": "Wadgaon", "short_name": "Wadgaon", "types": ["locality"]},
{"long_name": "Wardha", "short_name": "Wardha", "types": ["administrative_area_level_3"]},
{"long_name": "Nagpur Division", "short_name": "Nagpur Division", "types": ["administrative_area_level_2"]},
{"long_name": "Maharashtra", "short_name": "MH", "types": ["administrative_area_level_1"]},
{"long_name": "India", "short_name": "IN", "types": ["country"]},
{"long_name": "442301", "short_name": "442301", "types": ["postal_code"]}
],
"formatted_address": "144, Khattalwada, Wadgaon, Maharashtra 442301, India",
"geometry": {
"location": {"lat": 20.5937296, "lng": 78.9629344},
"location_type": "ROOFTOP"
},
"place_id": "ChIJc_ilQQBn0zsRKuQxPq-IF2c",
"types": ["establishment", "point_of_interest"]
}
✅ Pros:
- Extremely detailed—every level of address hierarchy
- Returns multiple results (pick the most relevant)
- Includes landmarks, establishments, plus codes
- More accurate for specific buildings
- Full control over what to display
❌ Cons:
- Requires API calls (watch your quota)
- Response structure is complex to parse
- Need to manage API keys
When to use it: When you need precise addresses, landmark names, or want full control over address formatting.
🧩 Understanding address_components (The Confusing Part)
When I first saw Google's response, I was like... what is all this?
Here's the deal: Google breaks down every address into granular components—from the smallest detail (building number) to the largest (country).
Each component has types that tell you what kind of address part it is.
Let me decode this for you:
| Type | What It Means | Example |
|---|---|---|
street_number |
Building/house number | 144 |
route |
Street/road name | MG Road |
sublocality / sublocality_level_1
|
Neighborhood/area | Khattalwada |
locality |
City/town | Wadgaon |
administrative_area_level_3 |
District/taluk | Wardha |
administrative_area_level_2 |
Division (group of districts) | Nagpur Division |
administrative_area_level_1 |
State/province | Maharashtra |
country |
Country | India |
postal_code |
ZIP/PIN code | 442301 |
establishment |
Business/landmark | Café Coffee Day |
point_of_interest |
Notable location | Central Park |
plus_code |
Google's grid-based code | QPHX+RWG |
🧠 Why Google Returns Multiple Results
Here's something that confused me at first—why does Google return an array of results instead of just one?
Because the same coordinates can represent different levels of specificity:
-
Exact building (if mapped) →
"144, Khattalwada" -
Nearest street →
"Khattalwada Road" -
General area →
"Wadgaon, Maharashtra" -
Administrative region →
"Wardha District"
Google gives you all possible interpretations, ordered by relevance.
Usually, data.results[0] is the most precise. But sometimes you might want a different one—for example:
- First result is just a plus code? Skip to the second.
- First result is too specific (building name)? Use the third (street name).
That's why having the full array is powerful—you get to choose.
💡 Parsing Smart: Getting What You Actually Need
Don't want the whole mess? Here's how to extract just what matters.
Get Just City and State:
const getSimpleAddress = (addressComponents) => {
const city = addressComponents.find(c =>
c.types.includes("locality")
)?.long_name || "";
const state = addressComponents.find(c =>
c.types.includes("administrative_area_level_1")
)?.long_name || "";
return `${city}, ${state}`;
};
// Usage
const simpleAddress = getSimpleAddress(bestResult.address_components);
// Output: "Wadgaon, Maharashtra"
Get Full Formatted Address (Easy Mode):
setAddress(bestResult.formatted_address);
// Output: "144, Khattalwada, Wadgaon, Maharashtra 442301, India"
Get Specific Components (Custom):
const getDetailedAddress = (addressComponents) => {
const street = addressComponents.find(c =>
c.types.includes("route")
)?.long_name || "";
const area = addressComponents.find(c =>
c.types.includes("sublocality")
)?.long_name || "";
const city = addressComponents.find(c =>
c.types.includes("locality")
)?.long_name || "";
return `${street}, ${area}, ${city}`;
};
// Output: "Khattalwada Road, Khattalwada, Wadgaon"
Pick and choose based on your app's needs.
🎯 Which Method Should You Actually Use?
Here's my honest recommendation:
Use Expo Location if:
- ✅ You want something simple and quick
- ✅ You're okay with basic info (city, state, postal code)
- ✅ You don't want to manage API quotas
- ✅ Your app doesn't need landmarks or precise building names
Use Google Geocoding API if:
- ✅ You need detailed, precise addresses
- ✅ You want landmarks and establishment names
- ✅ You need to customize which address components to show
- ✅ Your app already uses Google Maps API (you're paying anyway)
- ✅ You want control over multiple result options
🔥 My Approach: Best of Both Worlds
Here's what I actually do in production—use both methods with Google as primary and Expo as fallback:
const handleRegionChangeComplete = async (newRegion: any) => {
setRegion(newRegion);
try {
// Try Google API first (more detailed)
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${newRegion.latitude},${newRegion.longitude}&key=${googleApiKey}`
);
const data = await response.json();
const bestResult = data.results[0];
// Fallback to Expo Location
const [place] = await Location.reverseGeocodeAsync({
latitude: newRegion.latitude,
longitude: newRegion.longitude,
});
console.log("📍 Google result:", bestResult);
console.log("📍 Expo result:", place);
// Use Google if available, otherwise Expo
if (bestResult?.formatted_address) {
setAddress(bestResult.formatted_address);
} else if (place) {
setAddress(place.formattedAddress);
}
} catch (error) {
console.log("Error fetching place:", error);
}
};
Why this works:
- Google gives you rich, detailed info 90% of the time
- Expo catches the edge cases when Google fails
- Users always see something instead of blank addresses
- You're covered even if API limits hit
🚀 Putting It All Together
Here's the complete working example with everything we discussed:
Reverse geocoding sounds complicated, but once you understand the two approaches, it's actually pretty straightforward.
Start with Expo's reverseGeocodeAsync if you're just testing or need basic info. Upgrade to Google's Geocoding API when you need more control and detail.
Final Thoughts
Look, I'm not gonna lie—getting maps working in React Native was more work than I expected. But once I understood the why behind each step, it all clicked.
If you're stuck on a blank gray screen right now, trust me—you're probably just one small config fix away from getting it working. Double-check your API key, your SHA-1, your manifest. Clean and rebuild. Uninstall and reinstall if you have to.
And when it finally loads? When you see that map render with your markers and your custom styling? Honestly, it's worth the hassle.
Good luck. You got this. ☕
Questions? Hit me up in the comments. I'll try to help if I can.
Top comments (0)