Adding a Map and Markers to a React Application

Overview

This tutorial shows you how to add a Map and Marker to a React application using @googlemaps/react-wrapper and integrate the map and markers into the application state.

Install @googlemaps/react-wrapper

Install and use the @googlemaps/react-wrapper library to dynamically load the Maps JavaScript API when the component is rendered.

npm install @googlemaps/react-wrapper

This library can be imported and used with the following.

import { Wrapper, Status } from "@googlemaps/react-wrapper";

The basic usage of this component is to wrap child components that depend on Maps JavaScript API. The Wrapper component also accepts a render prop for rendering loading components or handling errors in loading the Maps JavaScript API.

const render = (status: Status) => {
  return <h1>{status}</h1>;
};

<Wrapper apiKey={"YOUR_API_KEY"} render={render}>
  <YourComponent/>
</Wrapper>

Add a map component

A basic functional component to render a map will likely make use of the useRef, useState, and useEffect React hooks.

The initial map component will have the following signature.

const Map: React.FC<{}> = () => {};

Because google.maps.Map requires an Element as a constructor parameter, useRef is needed to maintain a mutable object that will persist for the lifetime of the component. The following snippet instantiates a map within the useEffect hook in the body of the Map component.

const ref = React.useRef<HTMLDivElement>(null);
const [map, setMap] = React.useState<google.maps.Map>();

React.useEffect(() => {
  if (ref.current && !map) {
    setMap(new window.google.maps.Map(ref.current, {}));
  }
}, [ref, map]);

The above useEffect hook will only run when the ref has changed. The Map component now returns the following.

return <div ref={ref} />

Extend map component with additional props

The basic map component can be extended with additional props for map otions, event listeners, and styles applied to the div containing the map. The following code shows the expanded interface of this functional component.

interface MapProps extends google.maps.MapOptions {
  style: { [key: string]: string };
  onClick?: (e: google.maps.MapMouseEvent) => void;
  onIdle?: (map: google.maps.Map) => void;
}

const Map: React.FC<MapProps> = ({
  onClick,
  onIdle,
  children,
  style,
  ...options
}) => {}

The style object can passed directly through and set as a prop on the div rendered.

return <div ref={ref} style={style} />;

The onClick, onIdle, and google.maps.MapOptions require useEffect hooks to imperatively apply updates to the google.maps.Map.

// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
  if (map) {
    map.setOptions(options);
  }
}, [map, options]);

The event listeners require slightly more complex code to clear existing listeners when a handler passed as a prop has been updated.

React.useEffect(() => {
  if (map) {
    ["click", "idle"].forEach((eventName) =>
      google.maps.event.clearListeners(map, eventName)
    );

    if (onClick) {
      map.addListener("click", onClick);
    }

    if (onIdle) {
      map.addListener("idle", () => onIdle(map));
    }
  }
}, [map, onClick, onIdle]);

Build a marker component

The marker component uses similar patterns as the map component with useEffect and useState hooks.

const Marker: React.FC<google.maps.MarkerOptions> = (options) => {
  const [marker, setMarker] = React.useState<google.maps.Marker>();

  React.useEffect(() => {
    if (!marker) {
      setMarker(new google.maps.Marker());
    }

    // remove marker from map on unmount
    return () => {
      if (marker) {
        marker.setMap(null);
      }
    };
  }, [marker]);

  React.useEffect(() => {
    if (marker) {
      marker.setOptions(options);
    }
  }, [marker, options]);

  return null;
};

The component returns null as google.maps.Map manages the DOM manipulation.

Add markers as a child component of the map

To add the markers to a map, the Marker component will be passed to the Map component using the special children prop as in the following.

<Wrapper apiKey={"YOUR_API_KEY"}>
  <Map center={center} zoom={zoom}>
    <Marker position={position} />
  </Map>
</Wrapper>

A small change to the output of the Map component must be made to pass the google.maps.Map object to all children as an additional prop.

return (
  <>
    <div ref={ref} style={style} />
    {React.Children.map(children, (child) => {
      if (React.isValidElement(child)) {
        // set the map prop on the child component
        return React.cloneElement(child, { map });
      }
    })}
  </>
);

Link map and application state

Using the pattern above for onClick and onIdle callbacks, the application can be extended to fully integrate user actions such as clicking or panning the map.

const [clicks, setClicks] = React.useState<google.maps.LatLng[]>([]);
const [zoom, setZoom] = React.useState(3); // initial zoom
const [center, setCenter] = React.useState<google.maps.LatLngLiteral>({
  lat: 0,
  lng: 0,
});

const onClick = (e: google.maps.MapMouseEvent) => {
  // avoid directly mutating state
  setClicks([...clicks, e.latLng!]);
};

const onIdle = (m: google.maps.Map) => {
  console.log("onIdle");
  setZoom(m.getZoom()!);
  setCenter(m.getCenter()!.toJSON());
};

These hooks can be integrated into the form elements using the following pattern as demonstrated with the latitude input.

<label htmlFor="lat">Latitude</label>
<input
  type="number"
  id="lat"
  name="lat"
  value={center.lat}
  onChange={(event) =>
    setCenter({ ...center, lat: Number(event.target.value) })
  }
/>

Finally, the application can track clicks and render markers at each click location.

{clicks.map((latLng, i) => (<Marker key={i} position={latLng} />))}

Explore code

The complete sample code can be explored through the online code playgrounds below or by cloning the git repository.

Try Sample

Clone Sample

Git and Node.js are required to run this sample locally. Follow these instructions to install Node.js and NPM. The following commands clone, install dependencies and start the sample application.

  git clone -b sample-react-map https://github.com/googlemaps/js-samples.git
  cd js-samples
  npm i
  npm start

Other samples can be tried by switching to any branch beginning with sample-SAMPLE_NAME.

  git checkout sample-SAMPLE_NAME
  npm i
  npm start