DEV Community

Cover image for Build an Address Picker with Maps in React Native & Expo
Adhiraj Kar
Adhiraj Kar

Posted on

Build an Address Picker with Maps in React Native & Expo

A fully functional address picker with:

  • πŸ—ΊοΈ Interactive Google Map
  • πŸ“ Tap to select location
  • 🎯 Current GPS location button
  • πŸ” Search with autocomplete
  • πŸ“ Auto-fill address details

Prerequisites

You need:

  • Node.js (version 18+)
  • Expo CLI: npm install -g expo-cli
  • Google Cloud Account (free tier)
  • Basic React knowledge (useState, useEffect)

πŸš€ Part 1: Project Setup

Step 1: Get Your Google Maps API Key

This is crucial! Without this, your map won't work.

1.1 Create a Google Cloud Project

  1. Go to Google Cloud Console
  2. Click the project dropdown at the top
  3. Click "New Project"
  4. Name it: my-address-picker-app
  5. Click "Create"
  6. Wait for the notification that your project is created

1.2 Enable Required APIs

You need to enable THREE APIs:

  1. In the search bar, type "Maps SDK for Android"
  2. Click on it β†’ Click "Enable"
  3. Wait for it to enable (takes ~30 seconds)

Repeat for:

  • Maps SDK for iOS
  • Places API
  • Geocoding API

Why do we need these?

  • Maps SDK: Display the map
  • Places API: Search for locations
  • Geocoding API: Convert coordinates to addresses

1.3 Create API Key

  1. Go to "APIs & Services" β†’ "Credentials"
  2. Click "Create Credentials" β†’ "API Key"
  3. Copy your API key (looks like: AIzaSyB3daWjbihjieRz7vrxl6-ok82013WGTabsjE)
  4. IMPORTANT: Click "Restrict Key" to secure it
  5. Under "API restrictions", select:
    • Maps SDK for Android
    • Maps SDK for iOS
    • Places API
    • Geocoding API
  6. Click "Save"

1.4 Enable Billing

Google Maps requires billing info (but has a generous free tier):

  1. Go to "Billing" in the menu
  2. Link a billing account
  3. Don't worry: You get $200 free credit per month
  4. This tutorial's usage will likely cost $0

⚠️ Common Mistake: Skipping billing setup will cause "API key not valid" errors!


Step 2: Create Your Expo Project

Open your terminal and run:

# Create a new Expo project
npx create-expo-app my-address-picker --template blank

# Navigate into the project
cd my-address-picker

# Start the development server (to test everything works)
npx expo start
Enter fullscreen mode Exit fullscreen mode

What's happening here?

  • create-expo-app: Creates a new React Native project using Expo
  • --template blank: Starts with a minimal setup
  • expo start: Launches the development server

You should see a QR code in your terminal. Scan it with Expo Go app to see the default "Hello World" screen.

βœ… Checkpoint: If you see "Hello World" on your phone, you're good to go!


Step 3: Install Required Dependencies

Stop the server (Ctrl+C) and install packages:

npm install react-native-maps expo-location react-native-svg react-native-safe-area-context react-native-toast-message
Enter fullscreen mode Exit fullscreen mode

What each package does:

Package Purpose Why We Need It
react-native-maps Display interactive maps Core feature - shows the map
expo-location Access device GPS Get user's current location
react-native-svg Render custom icons Beautiful marker and button icons
react-native-safe-area-context Handle iPhone notches Prevent UI from hiding behind notch
react-native-toast-message Show success messages User feedback when saving

Step 4: Configure Your Project

4.1 Update app.json

Open app.json and replace its contents with:

{
  "expo": {
    "name": "Address Picker",
    "slug": "address-picker-app",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.yourname.addresspicker",
      "infoPlist": {
        "NSLocationWhenInUseUsageDescription": "We need your location to help you add addresses accurately.",
        "NSLocationAlwaysAndWhenInUseUsageDescription": "We need your location to help you add addresses accurately."
      },
      "config": {
        "googleMapsApiKey": "YOUR_API_KEY_HERE"
      }
    },
    "android": {
      "package": "com.yourname.addresspicker",
      "permissions": [
        "ACCESS_FINE_LOCATION",
        "ACCESS_COARSE_LOCATION"
      ],
      "config": {
        "googleMaps": {
          "apiKey": "YOUR_API_KEY_HERE"
        }
      }
    },
    "plugins": [
      [
        "react-native-maps",
        {
          "iosGoogleMapsApiKey": "YOUR_API_KEY_HERE",
          "androidGoogleMapsApiKey": "YOUR_API_KEY_HERE"
        }
      ]
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”΄ IMPORTANT: Replace YOUR_API_KEY_HERE (appears 4 times!) with your actual API key from Step 1.

What's happening here?

  • infoPlist: iOS permission messages users will see
  • permissions: Android permissions we need
  • plugins: Tells Expo to configure react-native-maps
  • googleMapsApiKey: Your API key for the map to work

4.2 Rebuild the Project

Since we added native dependencies, rebuild:

npx expo prebuild

# Then start again
npx expo run:ios
# or
npx expo run:android
Enter fullscreen mode Exit fullscreen mode

What's happening?

  • prebuild: Generates native iOS/Android code with our dependencies
  • You'll see new ios/ and android/ folders created

βœ… Checkpoint: Run npx expo start - it should start without errors.

πŸ’» Part 2: Building the Features

Now the fun part! We'll build this step by step.

Step 5: Create the Basic Map Screen

5.1 Create the File

Create a new file: app/AddAddressScreen.js

import React, { useState, useRef } from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import MapView, { PROVIDER_GOOGLE } from 'react-native-maps';

const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('window');

export default function AddAddressScreen() {
  // State to track the map's current region (visible area)
  const [mapRegion, setMapRegion] = useState({
    latitude: 28.6139,      // Default: Delhi, India
    longitude: 77.2090,
    latitudeDelta: 0.0922,  // Zoom level (smaller = more zoomed in)
    longitudeDelta: 0.0421,
  });

  // Reference to access map methods
  const mapRef = useRef(null);

  return (
    <View style={styles.container}>
      <MapView
        ref={mapRef}
        style={styles.map}
        initialRegion={mapRegion}
        provider={PROVIDER_GOOGLE}
        showsUserLocation={true}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F8F9FA',
  },
  map: {
    width: SCREEN_WIDTH,
    height: SCREEN_HEIGHT,
  },
});
Enter fullscreen mode Exit fullscreen mode

What's happening here?

Code Purpose
useState Creates a variable that triggers re-render when changed
mapRegion Stores what part of the world the map shows
latitude/longitude Center point of the map (Delhi coordinates)
latitudeDelta How much latitude to show (controls zoom)
useRef Creates a reference to call map methods later
PROVIDER_GOOGLE Use Google Maps (not Apple Maps)
showsUserLocation Shows blue dot for user's location

5.2 Update App.js to Show Your Screen

Replace App.js with:

import React from 'react';
import AddAddressScreen from './app/AddAddressScreen';

export default function App() {
  return <AddAddressScreen />;
}
Enter fullscreen mode Exit fullscreen mode

5.3 Test It!

npx expo run:ios
npx expo run:android
Enter fullscreen mode Exit fullscreen mode

Expected Result: You should see a full-screen Google Map centered on Delhi.

❌ Common Mistakes:

Problem Solution
Blank white screen Check API key is correct in app.json
"Google Maps not available" Run npx expo prebuild again
Map shows but is gray Enable billing in Google Cloud Console
"Invalid API key" Make sure all 4 APIs are enabled in Step 1.2

Step 6: Add the Center Marker

Let's add a fixed marker in the center that stays put while the map moves underneath.

6.1 Update AddAddressScreen.js

Add this import at the top:

import Svg, { Path, Circle } from 'react-native-svg';
Enter fullscreen mode Exit fullscreen mode

Add this component right after </MapView>:

{/* Center Marker - stays fixed while map moves */}
<View style={styles.centerMarker}>
  <Svg width={36} height={36} viewBox="0 0 24 24" fill="none">
    <Path
      d="M21 10C21 17 12 23 12 23S3 17 3 10A9 9 0 0 1 12 1A9 9 0 0 1 21 10Z"
      fill="#FF0000"
      stroke="white"
      strokeWidth="2"
    />
    <Circle cx="12" cy="10" r="3" fill="white" />
  </Svg>
</View>
Enter fullscreen mode Exit fullscreen mode

Add these styles:

centerMarker: {
  position: 'absolute',
  top: '50%',           // Center vertically
  left: '50%',          // Center horizontally
  marginTop: -36,       // Offset by half the marker height
  marginLeft: -18,      // Offset by half the marker width
  zIndex: 1,            // Show above map
  pointerEvents: 'none', // Allow touches to pass through to map
},
Enter fullscreen mode Exit fullscreen mode

What's happening here?

  • position: 'absolute': Takes the marker out of normal flow, lets us position it precisely
  • top: '50%', left: '50%': Center point of screen
  • marginTop/marginLeft: Adjust so the marker's point (not top-left) is at center
  • pointerEvents: 'none': Marker won't block map touches
  • Svg: Custom vector graphic for a nice-looking pin

Why not use MapView.Marker?

  • MapView.Marker moves with the map
  • We want a fixed overlay that stays centered
  • Better performance (no re-rendering when map moves)

6.2 Test It!

Expected Result: You should see a red pin marker fixed in the center. When you drag the map, the marker stays put!


Step 7: Add Current Location Button

Let's add a button that centers the map on the user's location.

7.1 Request Location Permission

Add these imports:

import { Alert, TouchableOpacity, ActivityIndicator } from 'react-native';
import * as Location from 'expo-location';
Enter fullscreen mode Exit fullscreen mode

Add these state variables after your existing useState:

const [locationPermission, setLocationPermission] = useState(false);
const [isGettingLocation, setIsGettingLocation] = useState(false);
const [selectedLocation, setSelectedLocation] = useState(null);
Enter fullscreen mode Exit fullscreen mode

Add this function (put it before the return statement):

// Request permission to access device location
const requestLocationPermission = async () => {
  try {
    // Ask user for permission
    const { status } = await Location.requestForegroundPermissionsAsync();

    // Store whether permission was granted
    setLocationPermission(status === 'granted');

    // If granted, get their location immediately
    if (status === 'granted') {
      getCurrentLocation();
    } else {
      Alert.alert(
        'Permission Denied',
        'We need location access to show your current position.'
      );
    }
  } catch (error) {
    console.error('Error requesting location permission:', error);
    Alert.alert('Error', 'Unable to access location services.');
  }
};
Enter fullscreen mode Exit fullscreen mode

What's happening here?

Code Purpose
requestForegroundPermissionsAsync() Shows iOS/Android permission popup
status === 'granted' User tapped "Allow"
Alert.alert() Shows a popup message to the user
try/catch Handles errors gracefully

7.2 Get Current Location

Add this function:

// Get user's current GPS coordinates
const getCurrentLocation = async () => {
  setIsGettingLocation(true); // Show loading indicator

  try {
    // Get GPS coordinates
    const location = await Location.getCurrentPositionAsync({
      accuracy: Location.Accuracy.High, // Use GPS, not just network
    });

    // Extract latitude and longitude
    const { latitude, longitude } = location.coords;

    // Create a new map region centered on user
    const newRegion = {
      latitude,
      longitude,
      latitudeDelta: 0.01,  // Zoom in closer
      longitudeDelta: 0.01,
    };

    // Animate the map to this location
    if (mapRef.current) {
      mapRef.current.animateToRegion(newRegion, 1000); // 1000ms animation
    }

    // Save the selected location
    setSelectedLocation({ latitude, longitude });

  } catch (error) {
    console.error('Error getting current location:', error);
    Alert.alert('Error', 'Unable to get your current location. Please try again.');
  } finally {
    setIsGettingLocation(false); // Hide loading indicator
  }
};
Enter fullscreen mode Exit fullscreen mode

What's happening here?

  • accuracy: Location.Accuracy.High: Uses GPS satellite for precise location
  • animateToRegion: Smoothly moves map to new location (not instant jump)
  • 1000: Animation duration in milliseconds (1 second)
  • finally: Runs whether success or error (perfect for hiding loaders)

7.3 Add the Button UI

Add this import:

import { Line } from 'react-native-svg';
Enter fullscreen mode Exit fullscreen mode

Add this button after the center marker (before the closing </View>):

{/* Current Location Button */}
<TouchableOpacity
  style={styles.currentLocationButton}
  onPress={getCurrentLocation}
  disabled={isGettingLocation}
>
  {isGettingLocation ? (
    <ActivityIndicator color="#FF0000" />
  ) : (
    <Svg width={24} height={24} viewBox="0 0 24 24" fill="none">
      <Line x1="2" x2="5" y1="12" y2="12" stroke="#FF0000" strokeWidth="2" />
      <Line x1="19" x2="22" y1="12" y2="12" stroke="#FF0000" strokeWidth="2" />
      <Line x1="12" x2="12" y1="2" y2="5" stroke="#FF0000" strokeWidth="2" />
      <Line x1="12" x2="12" y1="19" y2="22" stroke="#FF0000" strokeWidth="2" />
      <Circle cx="12" cy="12" r="7" stroke="#FF0000" strokeWidth="2" />
      <Circle cx="12" cy="12" r="3" stroke="#FF0000" strokeWidth="2" />
    </Svg>
  )}
</TouchableOpacity>
Enter fullscreen mode Exit fullscreen mode

Add these styles:

currentLocationButton: {
  position: 'absolute',
  right: 16,
  top: 100,
  backgroundColor: 'white',
  width: 48,
  height: 48,
  borderRadius: 24,
  justifyContent: 'center',
  alignItems: 'center',
  shadowColor: '#000',
  shadowOffset: { width: 0, height: 2 },
  shadowOpacity: 0.2,
  shadowRadius: 4,
  elevation: 5, // Android shadow
},
Enter fullscreen mode Exit fullscreen mode

7.4 Call Permission on Mount

Add this useEffect after your functions:

import { useEffect } from 'react'; // Add to imports

// Run once when component loads
useEffect(() => {
  requestLocationPermission();
}, []); // Empty array = run once on mount
Enter fullscreen mode Exit fullscreen mode

What's happening here?

  • useEffect: Runs code at specific times
  • []: Empty dependency array means "run once when component first loads"
  • Automatically asks for permission when user opens the screen

7.5 Test It!

Expected Result:

  1. App asks for location permission
  2. You see a white circular button in the top-right
  3. Tap it β†’ map animates to your current location
  4. While loading, button shows a spinner

❌ Common Mistakes:

Problem Solution
Permission not requested Check useEffect is called correctly
"Location services disabled" Enable GPS in phone settings
Button doesn't show Check styles.currentLocationButton is defined
Crashes on button press Wrap in try/catch as shown

Step 8: Add Search with Autocomplete

Now let's add a search bar with Google Places suggestions.

8.1 Add Search State

Add these state variables:

const [searchQuery, setSearchQuery] = useState('');
const [suggestions, setSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [isSearching, setIsSearching] = useState(false);
Enter fullscreen mode Exit fullscreen mode

8.2 Create Search Function

// Search for places using Google Places API
const searchPlaces = async (query) => {
  // Don't search if query is too short
  if (!query.trim() || query.length < 3) {
    setSuggestions([]);
    setShowSuggestions(false);
    return;
  }

  try {
    // Replace with your actual API key
    const API_KEY = 'YOUR_API_KEY_HERE';

    // Build the API URL
    const url = `https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent(query)}&key=${API_KEY}&components=country:in`;

    // Make the request
    const response = await fetch(url);
    const data = await response.json();

    // Check if we got results
    if (data.status === 'OK' && data.predictions) {
      // Show up to 5 suggestions
      setSuggestions(data.predictions.slice(0, 5));
      setShowSuggestions(true);
    } else if (data.status === 'ZERO_RESULTS') {
      setSuggestions([]);
      setShowSuggestions(false);
    } else {
      console.error('Places API error:', data.status);
    }
  } catch (error) {
    console.error('Search error:', error);
  }
};
Enter fullscreen mode Exit fullscreen mode

What's happening here?

Code Purpose
query.trim() Remove spaces from start/end
query.length < 3 Wait for 3+ characters before searching
encodeURIComponent() Make query URL-safe (handles spaces, special chars)
components=country:in Limit results to India (change to your country)
slice(0, 5) Show only first 5 results

8.3 Add Debouncing

Debouncing prevents API calls on every keystroke:

import { useCallback } from 'react'; // Add to imports

// Debounced search - waits 300ms after user stops typing
const debouncedSearch = useCallback(
  (() => {
    let timeoutId;
    return (query) => {
      // Cancel previous timeout
      clearTimeout(timeoutId);

      // Set new timeout
      timeoutId = setTimeout(() => {
        searchPlaces(query);
      }, 300); // Wait 300ms
    };
  })(),
  [] // No dependencies - function stays the same
);

// Handle search input change
const handleSearchChange = (text) => {
  setSearchQuery(text);
  debouncedSearch(text); // Call debounced version
};
Enter fullscreen mode Exit fullscreen mode

What's debouncing?

Without debouncing:

  • User types "pizza" β†’ 5 API calls (p, pi, piz, pizz, pizza)
  • Expensive! Slow! Hits rate limits!

With debouncing:

  • User types "pizza" β†’ 1 API call (after they stop typing)
  • Waits 300ms after last keystroke
  • Much better!

8.4 Get Place Details

When user taps a suggestion, we need to get its coordinates:

// Get coordinates for a selected place
const selectPlace = async (place) => {
  setSearchQuery(place.description);
  setShowSuggestions(false);
  setIsSearching(true);

  try {
    const API_KEY = 'YOUR_API_KEY_HERE';

    // Get detailed info about the place
    const url = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${place.place_id}&fields=geometry&key=${API_KEY}`;

    const response = await fetch(url);
    const data = await response.json();

    if (data.result?.geometry?.location) {
      const { lat, lng } = data.result.geometry.location;

      // Move map to this location
      const newRegion = {
        latitude: lat,
        longitude: lng,
        latitudeDelta: 0.01,
        longitudeDelta: 0.01,
      };

      if (mapRef.current) {
        mapRef.current.animateToRegion(newRegion, 1000);
      }

      setSelectedLocation({ latitude: lat, longitude: lng });
    }
  } catch (error) {
    console.error('Place details error:', error);
    Alert.alert('Error', 'Unable to get location details.');
  } finally {
    setIsSearching(false);
  }
};
Enter fullscreen mode Exit fullscreen mode

8.5 Add Search UI

Add these imports:

import { TextInput, ScrollView, SafeAreaView } from 'react-native';
Enter fullscreen mode Exit fullscreen mode

Add this code right after the opening <View style={styles.container}>:

{/* Search Bar */}
<SafeAreaView style={styles.searchContainer}>
  <View style={styles.searchBar}>
    <TextInput
      style={styles.searchInput}
      placeholder="Search for a place..."
      value={searchQuery}
      onChangeText={handleSearchChange}
      returnKeyType="search"
    />
    {searchQuery.length > 0 && (
      <TouchableOpacity
        onPress={() => {
          setSearchQuery('');
          setSuggestions([]);
          setShowSuggestions(false);
        }}
        style={styles.clearButton}
      >
        <Text style={styles.clearButtonText}>βœ•</Text>
      </TouchableOpacity>
    )}
  </View>

  {/* Suggestions Dropdown */}
  {showSuggestions && suggestions.length > 0 && (
    <View style={styles.suggestionsContainer}>
      <ScrollView style={styles.suggestionsList}>
        {suggestions.map((place) => (
          <TouchableOpacity
            key={place.place_id}
            style={styles.suggestionItem}
            onPress={() => selectPlace(place)}
          >
            <Text style={styles.suggestionText}>πŸ“ {place.description}</Text>
          </TouchableOpacity>
        ))}
      </ScrollView>
    </View>
  )}
</SafeAreaView>
Enter fullscreen mode Exit fullscreen mode

Add these styles:

searchContainer: {
  position: 'absolute',
  top: 0,
  left: 0,
  right: 0,
  zIndex: 10,
  padding: 16,
},
searchBar: {
  flexDirection: 'row',
  backgroundColor: 'white',
  borderRadius: 12,
  paddingHorizontal: 16,
  paddingVertical: 12,
  shadowColor: '#000',
  shadowOffset: { width: 0, height: 2 },
  shadowOpacity: 0.1,
  shadowRadius: 4,
  elevation: 3,
  alignItems: 'center',
},
searchInput: {
  flex: 1,
  fontSize: 16,
  color: '#333',
},
clearButton: {
  padding: 4,
},
clearButtonText: {
  fontSize: 18,
  color: '#999',
},
suggestionsContainer: {
  backgroundColor: 'white',
  borderRadius: 12,
  marginTop: 8,
  maxHeight: 200,
  shadowColor: '#000',
  shadowOffset: { width: 0, height: 2 },
  shadowOpacity: 0.1,
  shadowRadius: 4,
  elevation: 3,
},
suggestionsList: {
  maxHeight: 200,
},
suggestionItem: {
  padding: 16,
  borderBottomWidth: 1,
  borderBottomColor: '#F0F0F0',
},
suggestionText: {
  fontSize: 14,
  color: '#333',
},
Enter fullscreen mode Exit fullscreen mode

8.6 Test It!

Expected Result:

  1. Type "coffee" in the search bar
  2. After 300ms, see suggestions appear
  3. Tap a suggestion β†’ map moves to that location
  4. See the βœ• button to clear search

Step 9: Add Reverse Geocoding

Reverse geocoding = converting coordinates to an address.

9.1 Add State for Address

const [displayAddress, setDisplayAddress] = useState('');
const [isFetchingAddress, setIsFetchingAddress] = useState(false);
const [addressDetails, setAddressDetails] = useState({
  streetNumber: '',
  route: '',
  area: '',
  city: '',
  pincode: '',
  state: '',
});
Enter fullscreen mode Exit fullscreen mode

9.2 Create Reverse Geocoding Function

// Convert coordinates to address
const getAddressFromCoordinates = async (latitude, longitude) => {
  setIsFetchingAddress(true);

  try {
    const API_KEY = 'YOUR_API_KEY_HERE';
    const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${API_KEY}`;

    const response = await fetch(url);
    const data = await response.json();

    if (data.status === 'OK' && data.results && data.results.length > 0) {
      const result = data.results[0];

      // Get the formatted address
      setDisplayAddress(result.formatted_address);

      // Extract detailed components
      const components = result.address_components;
      let details = {
        streetNumber: '',
        route: '',
        area: '',
        city: '',
        pincode: '',
        state: '',
      };

      components.forEach((component) => {
        const types = component.types;

        if (types.includes('street_number')) {
          details.streetNumber = component.long_name;
        }
        if (types.includes('route')) {
          details.route = component.long_name;
        }
        if (types.includes('sublocality_level_1') || types.includes('sublocality')) {
          details.area = component.long_name;
        }
        if (types.includes('locality')) {
          details.city = component.long_name;
        }
        if (types.includes('postal_code')) {
          details.pincode = component.long_name;
        }
        if (types.includes('administrative_area_level_1')) {
          details.state = component.long_name;
        }
      });

      setAddressDetails(details);

      console.log('Address found:', result.formatted_address);
      console.log('Details:', details);
    }
  } catch (error) {
    console.error('Reverse geocoding error:', error);
  } finally {
    setIsFetchingAddress(false);
  }
};
Enter fullscreen mode Exit fullscreen mode

What's happening here?

Google returns address in components like:

{
  "street_number": "123",
  "route": "Main Street",
  "locality": "Mumbai",
  "postal_code": "400001"
}
Enter fullscreen mode Exit fullscreen mode

We extract each piece and store it separately.

9.3 Trigger on Map Movement

Update your MapView component:

<MapView
  ref={mapRef}
  style={styles.map}
  initialRegion={mapRegion}
  provider={PROVIDER_GOOGLE}
  showsUserLocation={locationPermission}
  onRegionChangeComplete={(region) => {
    setMapRegion(region);
    setSelectedLocation({
      latitude: region.latitude,
      longitude: region.longitude,
    });

    // Get address when map stops moving
    getAddressFromCoordinates(region.latitude, region.longitude);
  }}
/>
Enter fullscreen mode Exit fullscreen mode

What's happening?

  • onRegionChangeComplete: Fires when user stops dragging the map
  • Automatically gets the address for the center location
  • Real-time address updates as you move the map!

9.4 Add Address Display

Add this component above the current location button:

{/* Address Display */}
{displayAddress ? (
  <View style={styles.addressCard}>
    <Text style={styles.addressLabel}>Selected Location:</Text>
    <Text style={styles.addressText}>{displayAddress}</Text>
    {isFetchingAddress && (
      <ActivityIndicator size="small" color="#FF0000" style={{ marginTop: 8 }} />
    )}
  </View>
) : null}
Enter fullscreen mode Exit fullscreen mode

Add these styles:

addressCard: {
  position: 'absolute',
  bottom: 100,
  left: 16,
  right: 16,
  backgroundColor: 'white',
  padding: 16,
  borderRadius: 12,
  shadowColor: '#000',
  shadowOffset: { width: 0, height: 2 },
  shadowOpacity: 0.2,
  shadowRadius: 4,
  elevation: 5,
},
addressLabel: {
  fontSize: 12,
  color: '#666',
  marginBottom: 4,
},
addressText: {
  fontSize: 14,
  color: '#333',
  fontWeight: '500',
},
Enter fullscreen mode Exit fullscreen mode

9.5 Test It!

Expected Result:

  1. Drag the map around
  2. When you stop, see the address update
  3. Shows a loading indicator while fetching
  4. Address appears in a white card at the bottom

Step 10: Add Address Form

Now let's create a form to save the address.

10.1 Add Form State

const [formData, setFormData] = useState({
  houseNo: '',
  area: '',
  pincode: '',
  city: '',
  state: '',
  addressType: 'Home', // Home, Work, or Other
});
const [formErrors, setFormErrors] = useState({});
Enter fullscreen mode Exit fullscreen mode

10.2 Create Bottom Sheet

Install one more package:

npm install react-native-reanimated
Enter fullscreen mode Exit fullscreen mode

Add this simpler approach - a fixed bottom form:

{/* Address Form */}
<View style={styles.formContainer}>
  <ScrollView style={styles.formScroll}>
    <Text style={styles.formTitle}>Address Details</Text>

    {/* House Number */}
    <View style={styles.inputGroup}>
      <Text style={styles.inputLabel}>House/Flat/Floor No. *</Text>
      <TextInput
        style={[styles.input, formErrors.houseNo && styles.inputError]}
        placeholder="Enter house number"
        value={formData.houseNo}
        onChangeText={(text) => setFormData({ ...formData, houseNo: text })}
      />
      {formErrors.houseNo && (
        <Text style={styles.errorText}>{formErrors.houseNo}</Text>
      )}
    </View>

    {/* Area */}
    <View style={styles.inputGroup}>
      <Text style={styles.inputLabel}>Road/Area *</Text>
      <TextInput
        style={[styles.input, formErrors.area && styles.inputError]}
        placeholder="Enter area"
        value={formData.area}
        onChangeText={(text) => setFormData({ ...formData, area: text })}
      />
      {formErrors.area && (
        <Text style={styles.errorText}>{formErrors.area}</Text>
      )}
    </View>

    {/* Pincode */}
    <View style={styles.inputGroup}>
      <Text style={styles.inputLabel}>Pincode *</Text>
      <TextInput
        style={[styles.input, formErrors.pincode && styles.inputError]}
        placeholder="Enter pincode"
        value={formData.pincode}
        onChangeText={(text) => setFormData({ ...formData, pincode: text })}
        keyboardType="numeric"
        maxLength={6}
      />
      {formErrors.pincode && (
        <Text style={styles.errorText}>{formErrors.pincode}</Text>
      )}
    </View>

    {/* City */}
    <View style={styles.inputGroup}>
      <Text style={styles.inputLabel}>City *</Text>
      <TextInput
        style={[styles.input, formErrors.city && styles.inputError]}
        placeholder="Enter city"
        value={formData.city}
        onChangeText={(text) => setFormData({ ...formData, city: text })}
      />
      {formErrors.city && (
        <Text style={styles.errorText}>{formErrors.city}</Text>
      )}
    </View>

    {/* State */}
    <View style={styles.inputGroup}>
      <Text style={styles.inputLabel}>State *</Text>
      <TextInput
        style={[styles.input, formErrors.state && styles.inputError]}
        placeholder="Enter state"
        value={formData.state}
        onChangeText={(text) => setFormData({ ...formData, state: text })}
      />
      {formErrors.state && (
        <Text style={styles.errorText}>{formErrors.state}</Text>
      )}
    </View>

    {/* Address Type */}
    <View style={styles.inputGroup}>
      <Text style={styles.inputLabel}>Save as</Text>
      <View style={styles.typeButtons}>
        {['Home', 'Work', 'Other'].map((type) => (
          <TouchableOpacity
            key={type}
            style={[
              styles.typeButton,
              formData.addressType === type && styles.typeButtonActive,
            ]}
            onPress={() => setFormData({ ...formData, addressType: type })}
          >
            <Text
              style={[
                styles.typeButtonText,
                formData.addressType === type && styles.typeButtonTextActive,
              ]}
            >
              {type}
            </Text>
          </TouchableOpacity>
        ))}
      </View>
    </View>

    {/* Save Button */}
    <TouchableOpacity
      style={styles.saveButton}
      onPress={handleSaveAddress}
    >
      <Text style={styles.saveButtonText}>Save Address</Text>
    </TouchableOpacity>
  </ScrollView>
</View>
Enter fullscreen mode Exit fullscreen mode

Add these styles:

formContainer: {
  position: 'absolute',
  bottom: 0,
  left: 0,
  right: 0,
  backgroundColor: 'white',
  borderTopLeftRadius: 20,
  borderTopRightRadius: 20,
  maxHeight: SCREEN_HEIGHT * 0.6,
  shadowColor: '#000',
  shadowOffset: { width: 0, height: -2 },
  shadowOpacity: 0.1,
  shadowRadius: 8,
  elevation: 10,
},
formScroll: {
  padding: 16,
},
formTitle: {
  fontSize: 20,
  fontWeight: 'bold',
  marginBottom: 16,
  color: '#333',
},
inputGroup: {
  marginBottom: 16,
},
inputLabel: {
  fontSize: 14,
  color: '#666',
  marginBottom: 6,
},
input: {
  backgroundColor: '#F8F9FA',
  borderRadius: 8,
  paddingHorizontal: 16,
  paddingVertical: 12,
  fontSize: 16,
  borderWidth: 1,
  borderColor: '#E0E0E0',
},
inputError: {
  borderColor: '#FF0000',
},
errorText: {
  color: '#FF0000',
  fontSize: 12,
  marginTop: 4,
},
typeButtons: {
  flexDirection: 'row',
  gap: 8,
},
typeButton: {
  flex: 1,
  paddingVertical: 12,
  paddingHorizontal: 16,
  borderRadius: 8,
  borderWidth: 1,
  borderColor: '#E0E0E0',
  backgroundColor: 'white',
  alignItems: 'center',
},
typeButtonActive: {
  borderColor: '#FF0000',
  backgroundColor: '#FFF5F5',
},
typeButtonText: {
  fontSize: 14,
  color: '#666',
},
typeButtonTextActive: {
  color: '#FF0000',
  fontWeight: '600',
},
saveButton: {
  backgroundColor: '#FF0000',
  paddingVertical: 16,
  borderRadius: 12,
  alignItems: 'center',
  marginTop: 8,
  marginBottom: 32,
},
saveButtonText: {
  color: 'white',
  fontSize: 16,
  fontWeight: '600',
},
Enter fullscreen mode Exit fullscreen mode

10.3 Auto-fill Form from Address

Update getAddressFromCoordinates to auto-fill the form:

// After setting addressDetails, add:
setFormData({
  houseNo: details.streetNumber || formData.houseNo,
  area: details.area || details.route || formData.area,
  pincode: details.pincode || formData.pincode,
  city: details.city || formData.city,
  state: details.state || formData.state,
  addressType: formData.addressType, // Keep selected type
});
Enter fullscreen mode Exit fullscreen mode

10.4 Add Validation and Save

// Validate form fields
const validateForm = () => {
  const errors = {};

  if (!formData.houseNo.trim()) {
    errors.houseNo = 'House number is required';
  }
  if (!formData.area.trim()) {
    errors.area = 'Area is required';
  }
  if (!formData.pincode.trim()) {
    errors.pincode = 'Pincode is required';
  } else if (formData.pincode.length !== 6) {
    errors.pincode = 'Pincode must be 6 digits';
  }
  if (!formData.city.trim()) {
    errors.city = 'City is required';
  }
  if (!formData.state.trim()) {
    errors.state = 'State is required';
  }
  if (!selectedLocation) {
    errors.location = 'Please select a location on the map';
  }

  setFormErrors(errors);
  return Object.keys(errors).length === 0;
};

// Save the address
const handleSaveAddress = () => {
  if (!validateForm()) {
    Alert.alert('Validation Error', 'Please fill in all required fields');
    return;
  }

  // Here you would typically save to your backend
  const addressData = {
    ...formData,
    latitude: selectedLocation.latitude,
    longitude: selectedLocation.longitude,
    formattedAddress: displayAddress,
  };

  console.log('Saving address:', addressData);

  // Show success message
  Alert.alert(
    'Success!',
    'Address saved successfully',
    [
      {
        text: 'OK',
        onPress: () => {
          // Reset form or navigate back
          console.log('Address saved!');
        },
      },
    ]
  );
};
Enter fullscreen mode Exit fullscreen mode

10.5 Test It!

Expected Result:

  1. See a form at the bottom with address fields
  2. Move the map β†’ fields auto-fill
  3. Tap "Save Address" β†’ validation checks
  4. If valid β†’ shows success alert
  5. If invalid β†’ shows error messages in red

πŸ› Troubleshooting Guide

Map Issues

Problem: Blank white screen

  • Solution:
    1. Check API key is correct in app.json
    2. Verify billing is enabled in Google Cloud Console
    3. Make sure all 4 APIs are enabled
    4. Run npx expo prebuild again

Problem: Map is gray/blank tiles

  • Solution: Enable billing in Google Cloud Console

Problem: "Google Maps not available"

  • Solution: Run npx expo prebuild --clean then npx expo start

Location Issues

Problem: Permission modal doesn't appear

  • Solution: Check app.json has NSLocationWhenInUseUsageDescription (iOS) and ACCESS_FINE_LOCATION (Android)

Problem: Current location button doesn't work

  • Solution:
    1. Enable Location Services in device settings
    2. Grant app location permission
    3. Test on a real device (emulators have issues)

Problem: "Location timeout"

  • Solution:
    1. Make sure you're testing outdoors or near a window
    2. Increase timeout: Location.getCurrentPositionAsync({ timeout: 30000 })

Search Issues

Problem: No search suggestions appear

  • Solution:
    1. Check API key in searchPlaces function
    2. Verify Places API is enabled in Google Cloud Console
    3. Check console for error messages

Problem: "This API project is not authorized"

  • Solution:
    1. Go to Google Cloud Console
    2. APIs & Services β†’ Credentials
    3. Edit your API key
    4. Remove all restrictions temporarily to test
    5. Re-add restrictions once working

Problem: Search works but can't select location

  • Solution: Check the selectPlace function has correct API key

Reverse Geocoding Issues

Problem: Address not showing

  • Solution:
    1. Check API key in getAddressFromCoordinates
    2. Verify Geocoding API is enabled
    3. Check console.log for error messages

Problem: Address shows but doesn't update

  • Solution: Make sure onRegionChangeComplete is calling the function

Form Issues

Problem: Form doesn't auto-fill

  • Solution: Check setFormData is called in getAddressFromCoordinates

Problem: Validation not working

  • Solution: Verify validateForm() is called before handleSaveAddress

Problem: Keyboard covers inputs

  • Solution: Wrap form in KeyboardAvoidingView:
  <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)