DEV Community

Cover image for Stop Struggling with Maps in React Native — Here’s the Complete Guide
Arijit das
Arijit das

Posted on

Stop Struggling with Maps in React Native — Here’s the Complete Guide

☕ 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>
Enter fullscreen mode Exit fullscreen mode

...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
Enter fullscreen mode Exit fullscreen mode

Inside the <application> tag, paste this:

<meta-data
  android:name="com.google.android.geo.API_KEY"
  android:value="YOUR_GOOGLE_MAPS_API_KEY"/>
Enter fullscreen mode Exit fullscreen mode

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}"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💻 Run the signing report

On Windows (Command Prompt or PowerShell):

gradlew signingReport
Enter fullscreen mode Exit fullscreen mode

On macOS or Linux:

./gradlew signingReport
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 },
});
Enter fullscreen mode Exit fullscreen mode

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!")}
/>
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

You can also use your own custom image instead of the default red pin:

image={require('../assets/restaurant-pin.png')}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

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)"
/>
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

💡 What’s Happening Behind the Scenes

  1. Permission Request: The app politely asks for location access using expo-location.
  2. GPS Centering: One tap centers the map right on your current location.
  3. Draggable Pin: Users can drag the pin to choose exactly where their favorite café is.
  4. 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" }]
  }
]
Enter fullscreen mode Exit fullscreen mode

Then apply it:

import mapStyle from '../assets/map-style.json';

<MapView
  customMapStyle={mapStyle}
  style={{ flex: 1 }}
/>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

✅ 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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

✅ 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:

  1. Exact building (if mapped) → "144, Khattalwada"
  2. Nearest street"Khattalwada Road"
  3. General area"Wadgaon, Maharashtra"
  4. 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"
Enter fullscreen mode Exit fullscreen mode

Get Full Formatted Address (Easy Mode):

setAddress(bestResult.formatted_address);
// Output: "144, Khattalwada, Wadgaon, Maharashtra 442301, India"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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)