Skip to main content

How to Add Google Maps in React 2026

·APIScout Team
Share:

Google Maps is the most widely used mapping API. This guide covers the full integration: displaying maps, adding markers, geocoding addresses, showing directions, and adding place autocomplete — all in React with TypeScript.

What You'll Build

  • Interactive Google Map component
  • Custom markers with info windows
  • Address geocoding (text → coordinates)
  • Directions between two points
  • Place autocomplete search input

Prerequisites: React 18+, Google Cloud account, Maps JavaScript API enabled.

1. Setup

Get an API Key

  1. Go to Google Cloud Console
  2. Create a project (or select existing)
  3. Enable these APIs: Maps JavaScript API, Geocoding API, Directions API, Places API
  4. Create an API key under Credentials
  5. Restrict the key to your domain (important for production)

Install the Library

npm install @vis.gl/react-google-maps

This is the official Google-maintained React wrapper for Maps.

Environment Variables

# .env.local
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=AIza...

2. Basic Map

Map Provider

Wrap your app with the API provider:

// app/layout.tsx or providers.tsx
import { APIProvider } from '@vis.gl/react-google-maps';

export function MapProvider({ children }: { children: React.ReactNode }) {
  return (
    <APIProvider apiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!}>
      {children}
    </APIProvider>
  );
}

Display a Map

// components/BasicMap.tsx
'use client';
import { Map } from '@vis.gl/react-google-maps';

export function BasicMap() {
  return (
    <Map
      style={{ width: '100%', height: '500px' }}
      defaultCenter={{ lat: 37.7749, lng: -122.4194 }}
      defaultZoom={12}
      gestureHandling="greedy"
      disableDefaultUI={false}
    />
  );
}

3. Markers

Add Markers

// components/MapWithMarkers.tsx
'use client';
import { Map, AdvancedMarker, Pin } from '@vis.gl/react-google-maps';

const locations = [
  { id: 1, name: 'Stripe HQ', lat: 37.7900, lng: -122.3932 },
  { id: 2, name: 'GitHub HQ', lat: 37.7822, lng: -122.3912 },
  { id: 3, name: 'Twilio HQ', lat: 37.7862, lng: -122.3937 },
];

export function MapWithMarkers() {
  return (
    <Map
      style={{ width: '100%', height: '500px' }}
      defaultCenter={{ lat: 37.7860, lng: -122.3930 }}
      defaultZoom={15}
      mapId="your-map-id" // Required for AdvancedMarker
    >
      {locations.map((loc) => (
        <AdvancedMarker key={loc.id} position={{ lat: loc.lat, lng: loc.lng }}>
          <Pin background="#4285F4" glyphColor="#fff" borderColor="#2563eb" />
        </AdvancedMarker>
      ))}
    </Map>
  );
}

Markers with Info Windows

'use client';
import { useState } from 'react';
import { Map, AdvancedMarker, InfoWindow } from '@vis.gl/react-google-maps';

export function MapWithInfoWindows() {
  const [selectedId, setSelectedId] = useState<number | null>(null);

  return (
    <Map
      style={{ width: '100%', height: '500px' }}
      defaultCenter={{ lat: 37.7860, lng: -122.3930 }}
      defaultZoom={15}
      mapId="your-map-id"
    >
      {locations.map((loc) => (
        <AdvancedMarker
          key={loc.id}
          position={{ lat: loc.lat, lng: loc.lng }}
          onClick={() => setSelectedId(loc.id)}
        >
          {selectedId === loc.id && (
            <InfoWindow
              position={{ lat: loc.lat, lng: loc.lng }}
              onCloseClick={() => setSelectedId(null)}
            >
              <div>
                <h3>{loc.name}</h3>
                <p>Lat: {loc.lat}, Lng: {loc.lng}</p>
              </div>
            </InfoWindow>
          )}
        </AdvancedMarker>
      ))}
    </Map>
  );
}

4. Geocoding

Convert addresses to coordinates and vice versa:

// lib/geocode.ts
export async function geocodeAddress(address: string) {
  const response = await fetch(
    `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}`
  );
  const data = await response.json();

  if (data.results.length === 0) {
    throw new Error('Address not found');
  }

  const { lat, lng } = data.results[0].geometry.location;
  const formattedAddress = data.results[0].formatted_address;

  return { lat, lng, formattedAddress };
}

// Reverse geocoding (coordinates → address)
export async function reverseGeocode(lat: number, lng: number) {
  const response = await fetch(
    `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}`
  );
  const data = await response.json();
  return data.results[0]?.formatted_address ?? 'Unknown location';
}

Geocoding Component

'use client';
import { useState } from 'react';
import { geocodeAddress } from '@/lib/geocode';

export function AddressSearch({ onLocationFound }: {
  onLocationFound: (lat: number, lng: number) => void;
}) {
  const [address, setAddress] = useState('');

  const handleSearch = async () => {
    const { lat, lng } = await geocodeAddress(address);
    onLocationFound(lat, lng);
  };

  return (
    <div>
      <input
        value={address}
        onChange={(e) => setAddress(e.target.value)}
        placeholder="Enter an address..."
      />
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

5. Place Autocomplete

'use client';
import { useRef, useEffect, useState } from 'react';
import { useMapsLibrary } from '@vis.gl/react-google-maps';

export function PlaceAutocomplete({ onPlaceSelect }: {
  onPlaceSelect: (place: google.maps.places.PlaceResult) => void;
}) {
  const inputRef = useRef<HTMLInputElement>(null);
  const places = useMapsLibrary('places');

  useEffect(() => {
    if (!places || !inputRef.current) return;

    const autocomplete = new places.Autocomplete(inputRef.current, {
      fields: ['geometry', 'name', 'formatted_address'],
    });

    autocomplete.addListener('place_changed', () => {
      const place = autocomplete.getPlace();
      if (place.geometry?.location) {
        onPlaceSelect(place);
      }
    });
  }, [places, onPlaceSelect]);

  return (
    <input
      ref={inputRef}
      placeholder="Search for a place..."
      style={{ width: '300px', padding: '8px' }}
    />
  );
}

6. Directions

Show a route between two points:

'use client';
import { useEffect, useState } from 'react';
import { Map, useMapsLibrary, useMap } from '@vis.gl/react-google-maps';

function DirectionsRenderer({ origin, destination }: {
  origin: string;
  destination: string;
}) {
  const map = useMap();
  const routesLibrary = useMapsLibrary('routes');
  const [directionsResult, setDirectionsResult] = useState<
    google.maps.DirectionsResult | null
  >(null);

  useEffect(() => {
    if (!routesLibrary || !map) return;

    const directionsService = new routesLibrary.DirectionsService();
    const directionsRenderer = new routesLibrary.DirectionsRenderer({ map });

    directionsService.route(
      {
        origin,
        destination,
        travelMode: google.maps.TravelMode.DRIVING,
      },
      (result, status) => {
        if (status === 'OK' && result) {
          directionsRenderer.setDirections(result);
          setDirectionsResult(result);
        }
      }
    );

    return () => directionsRenderer.setMap(null);
  }, [routesLibrary, map, origin, destination]);

  if (!directionsResult) return null;

  const route = directionsResult.routes[0].legs[0];
  return (
    <div>
      <p>Distance: {route.distance?.text}</p>
      <p>Duration: {route.duration?.text}</p>
    </div>
  );
}

export function DirectionsMap() {
  return (
    <Map
      style={{ width: '100%', height: '500px' }}
      defaultCenter={{ lat: 37.7749, lng: -122.4194 }}
      defaultZoom={12}
    >
      <DirectionsRenderer
        origin="San Francisco, CA"
        destination="San Jose, CA"
      />
    </Map>
  );
}

Pricing

APIFree TierPer 1,000 Requests
Maps JavaScript$200/month credit$7.00 (loads)
Geocoding$200/month credit$5.00
Directions$200/month credit$5.00-$10.00
Places Autocomplete$200/month credit$2.83 per session

The $200/month free credit covers ~28,000 map loads or 40,000 geocoding requests.

Production Checklist

ItemNotes
Restrict API key to your domainPrevents unauthorized usage
Set billing alertsAvoid surprise charges
Enable only needed APIsReduce attack surface
Use server-side geocoding for batchDon't expose key in client for bulk operations
Cache geocoding resultsAddresses don't change — cache aggressively
Lazy load the map componentDon't load Maps JS until user scrolls to map

Alternatives to Google Maps

AlternativePricingBest For
Mapbox50K loads free, then $5/1KCustom map styles, 3D
HERE Maps250K transactions freeEnterprise, fleet management
OpenStreetMap + LeafletFree (open source)Budget projects, no vendor lock-in

Advanced Map Customization

Map IDs and styled maps are two of the most underused features in the Google Maps platform, and they unlock a level of visual control that's essential for production applications.

A Map ID is a unique identifier you create in Google Cloud Console under Maps > Map Styles. It serves two purposes: it links a map instance to a custom visual style, and it's a prerequisite for using AdvancedMarkers (the modern replacement for the legacy Marker class). Without a Map ID, you're stuck with the classic red pins. With one, you get full control over marker appearance through the Pin component.

// Create in Google Cloud Console > Maps > Map styles
// Then reference the Map ID in your component:
<Map
  style={{ width: '100%', height: '500px' }}
  defaultCenter={{ lat: 37.7749, lng: -122.4194 }}
  defaultZoom={12}
  mapId="your-map-id-from-cloud-console"  // Enables custom styles + AdvancedMarkers
/>

Map styles let you visually customize the appearance of every element on the map. You can hide transit lines and bus route labels for a cleaner look on an embedded map, change road colors to match your brand, remove POI labels (the coffee shop and restaurant pins that clutter the default view), and alter land and water colors to complement your UI. Styles are defined as a JSON array in the Cloud Console and referenced through the Map ID — you don't need to change any application code to update a style, which is convenient for design iteration.

For applications with large numbers of markers, clustering is the next step. When you drop 100 or more AdvancedMarker components on a map, the browser renders all of them simultaneously and performance degrades significantly. The @googlemaps/markerclusterer package solves this by grouping nearby markers into a single cluster badge showing the count. The @vis.gl/react-google-maps library integrates directly with this package through its MarkerClusterer component, so you can implement clustering without leaving the React component model.

Heatmaps are worth knowing about for density visualization use cases. Google Maps has a built-in visualization library that renders a gradient overlay showing where values are concentrated. This is particularly useful for delivery coverage maps (showing where your drivers actually go), service area maps (showing which zip codes you serve most), and analytics dashboards overlaid on geography. You load the visualization library with useMapsLibrary('visualization') and pass it a set of weighted lat/lng points.

Performance and Cost Optimization

Google Maps charges per map load, and costs can grow quickly on a high-traffic site. A page that embeds a map on every visit will consume your $200 monthly credit faster than you might expect — at $7 per 1,000 loads, the credit only covers about 28,000 map initializations per month.

The most effective cost reduction is lazy loading. The Maps JavaScript API should not load until the user actually sees the map. Using an Intersection Observer, you can defer loading the APIProvider until the map container scrolls into the viewport. This alone can cut your map load count in half on pages where the map appears below the fold, since a meaningful portion of users never scroll that far.

Avoid mounting the APIProvider at the application root if your app has many pages that don't use maps. The provider loads the Maps JavaScript API as soon as it mounts — placing it globally means every page load initializes the API even on pages with no maps.

For non-interactive use cases — order confirmation emails, preview thumbnails, static location displays — use the Static Maps API instead of the JavaScript API. Static Maps returns a PNG image via a simple HTTP request. It costs $2 per 1,000 requests versus $7 per 1,000 for the JavaScript API, and it has no client-side rendering overhead. A confirmation page showing "your delivery address" as a small map image is a perfect Static Maps use case.

Geocoding results should always be cached in your database. Addresses don't change, so the lat/lng for "123 Main St, San Francisco, CA" is valid indefinitely. A form that geocodes an address on submission should store the result alongside the address record. If the same user re-submits the form or you need to re-geocode for any reason, serve the cached coordinates rather than hitting the API again. This is especially important for batch operations like importing a CSV of addresses.

Set billing alerts in Google Cloud Console at multiple thresholds — $50, $100, and $200 are reasonable starting points. Runaway map loads from a misconfigured page or a traffic spike can rack up unexpected charges. Budget alerts give you an early warning before costs become significant.

When to Use Mapbox or OpenStreetMap Instead

Google Maps is the most familiar option for users and the most thoroughly documented option for developers, but it's not always the right choice for every project.

Mapbox is the better option when your application requires heavy visual customization, 3D map rendering, or indoor mapping. Mapbox Studio gives you granular control over every visual element of the map, down to individual road types and building heights. The Deck.gl integration makes Mapbox a natural fit for data visualization-heavy mapping products — think live flight tracking dashboards, logistics route visualizations, or real estate heat maps. Mapbox is also worth evaluating when you're operating at high volume and Google's per-request pricing starts to add up significantly.

OpenStreetMap with Leaflet is the right choice when you need zero cost at any volume, open source licensing with no terms-of-service restrictions, or the ability to self-host your map tiles. OpenStreetMap data is community-maintained and generally accurate for most use cases. Some B2B and government applications have constraints that rule out Google Maps — data sovereignty requirements, concerns about routing user location data through Google's servers, or procurement policies that prohibit Google products. In those cases, OpenStreetMap plus Leaflet (or MapTiler as a commercial tile provider built on OSM) gives you a fully capable mapping stack without Google's involvement.

The practical decision: if you need a standard map with geocoding and directions for a typical web application, Google Maps wins on documentation quality, reliability, and user familiarity. If you're building a visualization-heavy mapping product, evaluate Mapbox and Deck.gl. If you need to self-host map tiles, avoid vendor lock-in entirely, or have open source requirements, OpenStreetMap plus Leaflet or MapTiler is the path forward.

Pricing should not be the sole selection criterion at typical web application scales. All three platforms offer free tiers that comfortably support development and early production traffic. The platform you'll be debugging against at 2am — with quality documentation, a responsive community, and clear error messages — matters more than a cost differential that only materializes at volumes you haven't yet reached.


Building with maps? Compare Google Maps vs Mapbox vs HERE on APIScout — pricing, features, and developer experience compared.

Related: How to Add Geocoding to Your App with Mapbox, Google Maps vs HERE: Enterprise Location APIs, Google Maps vs HERE: Enterprise Location APIs

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.