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.
TypeScript
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]);
JavaScript
const ref = React.useRef(null); const [map, setMap] = React.useState(); 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 options, 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
.
TypeScript
// 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]);
JavaScript
// 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.
TypeScript
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]);
JavaScript
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.
TypeScript
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; };
JavaScript
const Marker = (options) => { const [marker, setMarker] = React.useState(); 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.
TypeScript
return ( <> <div ref={ref} style={style} /> {React.Children.map(children, (child) => { if (React.isValidElement(child)) { // set the map prop on the child component // @ts-ignore return React.cloneElement(child, { map }); } })} </> );
JavaScript
return ( <> <div ref={ref} style={style} /> {React.Children.map(children, (child) => { if (React.isValidElement(child)) { // set the map prop on the child component // @ts-ignore 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.
TypeScript
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()); };
JavaScript
const [clicks, setClicks] = React.useState([]); const [zoom, setZoom] = React.useState(3); // initial zoom const [center, setCenter] = React.useState({ lat: 0, lng: 0, }); const onClick = (e) => { // avoid directly mutating state setClicks([...clicks, e.latLng]); }; const onIdle = (m) => { 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