How To Write A Google Maps React Component
How To Write A Google Maps React Component
React Component
Ari Lerner
May 15, 2016 // 63 min read
In this tutorial, we'll walk through how to build a React component that
uses the Google Maps API.
Integrating React with external libraries like Google or Facebook APIs can be confusing
and challenging. In this discussion, we'll look at how to integrate the Google Maps API
with React. In this post we'll deal with lazily-loading the library through building
complex nested components.
This post is not only about how to use Google Maps, but how to use 3rd party libraries in
React generally and how to build up rich interactive components.
In this post, we'll look at how we to connect the Google API and build a Google Maps
Component.
Demo
Before we can integrate with Google Maps, we'll need to sign up at Google to get an API
key.
Show For more information about signing up for a Google Key of your own, check
the instructions here.
You're more than welcome to use our apiKey, but please use it lightly so
Google doesn't cut off our api access so it works for everyone.
Loading a Google-based
Component
In order to use Google within our components, we'll need to handle two technical
boundaries:
With our key in hand, we'll need to load up the Google API on our page. We can handle
this in multiple ways, including directly including the <script> tag on our page through
asynchronously loading the script using JavaScript. We try to keep our dependencies
limited to the scripts we directly need on a page as well as define our dependencies in
JavaScript, so we'll take the latter method of loading our window.google object using a
React component.
Sample usage:
this.scriptCache = cache({
google: 'https://api.google.com/some/script.js'
});
Sample usage:
GoogleApi({
apiKey: apiKey,
libraries: ['places']
});
Sample usage:
With our helpful scripts in-hand, we can load our Google Api in a Map component directly
in our React component. Let's do this together in building our Map:
The bulk of the work with the code is wrapped away in the GoogleApiComponent component.
It's responsible for passing through a loaded prop that is set to true after the Google API
has been loaded. Once it's loaded, the prop will be flipped to true and our default render
function will render the <div>.
We'll place our Map component inside this Container component using JSX. Since we're
using the GoogleApiComponent Higher-Order Component, we'll get a reference to a google
object and (in our case) a Google map. We can replace the currently
rendered <div> element with a reference to our Map component:
Before we move on, our map object won't show without a set height and width on the
containing object. Let's set one to be the entire page:
When our GoogleApiComponent loads on the page, it will create a google map component
and pass it into our Map component as a prop. As we're wrapping our main component
inside the Google api component wrapper, we can check for either a new prop or the
mounting of the component (we'll need to handle both) to see if/when we get a link to
the window.google library as it's been loaded on the page.
Let's update our Map component to include the case when the map is first loaded. When
the Map component is first loaded, we cannot depend upon the google api being available,
so we'll need to check if it's loaded. If our component is rendered without it,
the google prop will be undefined and when it's loaded, it will be defined.
loadMap() {
// ...
}
render() {
// ...
}
}
After a React component has updated, the componentDidUpdate() method will be run. Since
our component is based upon Google's api, which is outside of the React component
workflow, we can use the componentDidUpdate() method as a way to be confident our
component has changed and let the map update along with the rest of the component.
In our Map component, let's handle the case when the Map is available when the
component mounts. This would happen on the page whenever the map has already been
loaded previously in our app. For instance, the user navigated to a page with
a Map component already available.
loadMap() {
// ...
}
render() {
// ...
}
}
We'll need to define the loadMap() function to actually get any of our map on the page. In
here, we'll run the usual gapi functions to create a map. First, let's make sure
the google api is available. If it is, we'll be using the map key on the object, so let's extract
it here:
The loadMap() function is only called after the component has been rendered (i.e. there is a
DOM component on the page), so we'll need to grab a reference to the DOM component
where we want the map to be placed. In our render method, we have a <div> component
with a ref='map'. We can grab a reference to this component using the ReactDOM library:
The node variable above is a reference to the actual DOM element on the page, not the
virtual DOM, so we can set the google map to work with it directly as though we're using
plain JavaScript.
To instantiate a Google map object on our page, we'll use the map API (documentation is
here) as usual.
The maps.Map() constructor accepts a DOM node and a configuration object to create a
map. To instantiate a map we need at least two config options:
Above, we statically assigned the zoom and center (we'll move these to be dynamic
shortly).
Once we reload the page, we'll see that we now should have a map loaded in our page.
Demo
Adding props to
the Map Component
In order to make our center dynamic, we can pass it through as props (in fact, regardless
of how we'll be creating the center of the map, we'll pass the attributes through props).
Being good react developers, let's define our propTypes
Since we'll require the zoom and center to be present, we can define some default
properties to be set in case they aren't passed. Additionally, we can set them to be
required using the .isRequired argument on the PropType we're setting. As we'll make
these lat and lng dynamic using the browser's navigator object to find the current location,
we won't use the .isRequired object. Let's set some defaults on the Map:
Awesome. Now we can convert our loadMap() function to use these variables from
the this.props object instead of hardcoding them. Let's go ahead an update the method:
Adding state to
the Map Component
Since we'll be moving the map around and we'll want the map to retain state, we can
move this to be held in local state of the map. Moving the location to state will also have
the side-effect of making working with the navigator object simple.
We can update the loadMap() function to pull from the state, rather than from props:
Demo
Go
Lat:
Lng:
Awesome. We'll be using the navigator from the native browser implementation. We'll
need to be sure that the browser our user is using supports the navigator property, so
keeping that idea in mind, we can call on the Navigator object to get us the current
location of the user and update the state of our component to use this position object.
Additionally, let's only set the map to use the current location if we set a boolean prop to
true. It would be weird to use a <Map /> component with a center set to the current location
when we want to show a specific address.
Now, when the component itself mounts we can set up a callback to run to fetch the
current position. In our componentDidMount() function, let's add a callback to run and fetch
the current position:
Now when the map is mounted, the center will be updated... except, there's one problem:
the map won't be repositioned to the new location. The state will be updated, but the
center won't change. Let's fix this by checking for an update to the currentLocation in
the state after the component itself is updated.
We already have a componentDidUpdate() method defined, so let's use this spot to recenter
the map if the location changes.
recenterMap() {
// ...
}
// ...
}
The recenterMap() function will now only be called when the currentLocation in the
component's state is updated. Recentering the map is a straightforward process, we'll use
the .panTo() method on the google.maps.Map instance to change the center of the map:
if (map) {
let center = new maps.LatLng(curr.lat, curr.lng)
map.panTo(center)
}
}
// ...
}
Demo
For instance, when the google map has been moved or dragged around, we can fire a
callback. For instance, let's set up a callback to run when the map itself has been dragged
around.
To add event handlers, we need the map to be listening for events. We can add listeners
pretty easily with the Google API using the addListener() function on our Map.
After we create our map, in the loadMap() function, we can add our event listeners. Let's
handle the dragend event that will be fired when the user is done moving the map to a new
location.
When our user is done moving around the map, the dragend event will be fired and we'll
call our onMove() function we passed in with the props.
One issue with the way we're handling callbacks now is that the dragend event is fired a
LOT of times. We don't necessarily need it to be called every single time it's dragged
around, but at least once at the end. We can create a limit to the amount of times we'll call
the onMove() prop method by setting up a simple timeout that we can clear when the event
is fired again.
let centerChangedTimeout;
this.map.addListener('dragend', (evt) => {
if (centerChangedTimeout) {
clearTimeout(centerChangedTimeout);
centerChangedTimeout = null;
}
centerChangedTimeout = setTimeout(() => {
this.props.onMove(this.map);
}, 0);
})
}
// ...
}
}
Let's say we want to handle two events, the dragend event and the click event. Rather than
copy+pasting our code from above for every single event, let's build this up
programmatically.
First, let's create a list of the events we want to handle:
With our evtNames list, let's replace our addListener() funcitonality from above with a loop
for each of the evtNames:
evtNames.forEach(e => {
this.map.addListener(e, this.handleEvent(e));
});
}
// ...
}
handleEvent(evtName) {
}
}
As the addListener() function expects us to return an event handler function, we'll need to
return a function back, so we can start our handleEvent() function like:
We'll basically copy+paste our timeout functionality into our new handleEvent() function.
Now, any time we pass a prop with the event name, like click it will get called whenever
we click on the map itself. This isn't very React-like, or JS-like for that matter. Since it's a
callback, a better naming scheme would be onClick and onDragend.
Since we're going meta in the first place, let's make our propName be a camelized word
starting with on and ending with the capitalized event name.
With our camelize() helper function, we can replace the handlerName from
our handleEvent function:
Lastly, because we are good React-citizens, let's add these properties to our propTypes:
An example of this is giving the our <Map /> callback to trigger a ready event.
Let's add the 'ready' string to our evtNames so we handle the onReady prop (if passed in):
For handling the case after the map is ready (at the end of our loadMap() function), we can
call the trigger() function on the map instance with the event name.
evtNames.forEach(e => {
this.map.addListener(e, this.handleEvent(e));
});
maps.event.trigger(this.map, 'ready');
}
// ...
}
}
Since we've already set the rest of the event handlers up, this will just work.
Let's build a MarkerComponent using the React Way. As we previously did, let's build the
usage first and then build the implementation.
The React Way is to write our Marker components as children of the Map component.
We'll build our <Marker /> component as a child of the Map component so that they are
independent of the Map itself, but still can be interdependent upon the Map component being
available.
When we place a <Marker /> inside the <Map /> component, we'll want to pass through
some custom props that the Map contains, including the map instance object to it's children.
React gives us a convenient method for handling updating the props of children objects of
a component. First, let's update our Map.render() method to include rendering children:
render() {
return (
<div ref='map'>
Loading map...
{this.renderChildren()}
</div>
)
}
}
Now, when our <Map /> component is rendered, it will not only place the Map on the page,
but it will also call the lifecycle methods for it's children. Of course, we actually haven't
placed any children in the map yet.
The renderChildren() method will be responsible for actually calling the methods on the
children, so in here is where we'll create clones/copies of the children to display in the
map.
To add props to a child inside a component, we'll use the React.cloneElement() method.
This method accepts an element and creates a copy, giving us the opportunity to
append props and/or children to the child. We'll use the cloneElement() to append
the map instance, as well as the google prop. Additionally, let's add the map center as well,
so we can set the mapCenter as the default position of a marker.
Since we want the usage of children inside the Map component to be optional (so we can
support using the Map without needing children), let's return null if there are no children
passed to the Map instance:
Now, if we use the Map without children, the renderChildren() method won't blow up the
rest of the component. Moving on, we'll want to clone each of the children passed
through. In other words, we'll want to map through each of the children and run
the React.cloneElement() function on each.
React gives us the React.Children.map() to run over each of the children passed by a
component and run a function on... sounds suspiciously like what we need to do, ey?
Let's update our renderChildren() method to handle the cloning of our children:
if (!children) return;
Now, each of the Map component's children will not only receive their original props they
were passed, they will also receive the map instance, the google api instance, and
the mapCenter from the <Map /> component. Let's use this and build our MarkerComponent:
The Marker component is similar to the Map component in that it's a wrapper around the
google api, so we'll take the same strategy where we will update the raw JS object after
the component itself has been updated (via props or state).
Although we'll write a component that hands the constructed virtual DOM back to React,
we won't need to interact with the DOM element, so we can return null from our render
method (to prevent it from flowing into the view).
While we are at it, let's also define our propTypes for our Marker component. We'll need to
define a position at minimum.
Marker.propTypes = {
position: React.PropTypes.object,
map: React.PropTypes.object
}
With our propTypes set, let's get started wrapping our new component with
the google.maps.Marker() object. As we did with our previous Map component, we'll interact
with the component after it's props have been updated.
Our marker will need to be updated only when the position or the map props have
changed. Let's update our componentDidUpdate() function to run it's function only upon
these changes:
When we pass a position property, we'll want to grab that position and create a
new LatLng() object for it's elements. If no position is passed, we'll use the mapCenter. In
code, this looks like:
With our position object, we can create a new google.maps.Marker() object using these
preferences:
export class Marker extends React.Component {
renderMarker() {
let {
map, google, position, mapCenter
} = this.props;
const pref = {
map: map,
position: position
};
this.marker = new google.maps.Marker(pref);
}
// ...
}
After reloading our page, we'll see we have a few markers on the map.
Markers aren't too interesting without interactivity. Let's add some to our markers.
We can handle adding interactivity to our <Marker /> component in the exact same way as
we did with our <Map /> component.
Let's keep track of the names of the events we want to track with our Marker:
Back when we create the Marker instance, we can add functionality to handle the event:
evtNames.forEach(e => {
this.marker.addListener(e, this.handleEvent(e));
})
}
handleEvent(evtName) {
// ...
}
}
Our handleEvent() function will look nearly the same as the function in the <Map
/> component:
Demo
San Francisco
Edwardian Hotel
Travelodge by Wyndham San Francisco Central
Hayes Valley Inn
Zuni Café
SOMA Park Inn - Civic Center
Walgreens
Absinthe Brasserie & Bar
SFJAZZ
San Francisco Symphony
Rickshaw Stop
San Francisco Opera
PARISOMA
Blue Bottle Coffee
International High School of San Francisco
Transwestern
Hayes Street Grill
Pause Wine Bar in Hayes Valley
Burton Richard
SoMa
Map data ©2019 Google
Terms of Use
Removing Markers
When we're done with the markers, it's useful to remove them from the map. Since React
is taking care of the state tree, we can just ask the google API to remove the marker for us
using the setMap(null) function on the <Marker /> instance.
Adding a componentWillUnmount() function to the <Marker /> component will handle this
task for us:
We'll also make the Container stateful to hold on to the latest clicked
marker/info
export class Container extends React.Component {
getInitialState: function() {
return {
showingInfoWindow: false,
activeMarker: {},
selectedPlace: {}
}
},
render() {
const style = {
width: '100vw',
height: '100vh'
}
const pos = {lat: 37.759703, lng: -122.428093}
return (
<div style={style}>
<Map google={this.props.google}>
<Marker
onClick={this.onMarkerClick}
name={'Dolores park'}
position={pos} />
<InfoWindow
marker={this.state.activeMarker}
visible={this.state.showingInfoWindow}>
<div>
<h1>{this.state.selectedPlace.name}</h1>
</div>
</InfoWindow>
</Map>
</div>
)
}
}
// ...
When we click on the <Marker /> component, we'll call the onMarkerClick() function. Let's
go ahead and handle this event:
We'll handle updating the state of the component when we click on the <Marker /> above
Just like our <Marker /> component, our <InfoWindow /> component will mirror the state of
the Google map instance by updating itself along with the updates of of the map. Thus,
we'll update this component using the componentDidUpdate() lifecycle function.
We have three separate state cases to check for updates when updating
the InfoWindow component:
1. We need to check to see if we have a map instance available (as we did with
the <Marker /> component)
2. If the content of the InfoWindow has been updated so we can update it live.
3. We need to check to see if the state of the visibility of the InfoWindow has
changed.
updateContent() {}
}
renderChildren() {}
}
The infowindow requires us to set content for us to show in the browser. Previously, we set
the content to an empty string. When we want to show the window, the empty string isn't
going to be very interesting. We'll use the children of the <InfoWindow /> component to
define what the instance should show.
As we previously discussed, we'll need to translate the React component into an HTML
string that the InfoWindow instance knows how to handle. We can use
the ReactDOMServer from react-dom to update the content.
We can use this package to translate the children of the <InfoWindow /> component in
our renderChildren() function:
As we're checking against the previous props, we know that the infoWindow is closed if
the visible prop is true and visa versa.
In addition, if our user clicks on a new marker and the visibility has not changed,
the InfoWindow won't be updated. We can check the value of the marker along with the
visibility flag in the same spot:
openWindow() {}
closeWindow() {}
}
The openWindow() and closeWindow() functions are simple wrappers around the
google InfoWindow instance that we can use to call open() or close() on it:
If we head back to our browser, refresh, and click on a marker, we'll see that the
infoWindow is now showing
Demo
Map data ©2019 Google
Terms of Use
InfoWindow callbacks
Lastly, the state of the InfoWindow this.state.showingInfoWindow will never be reset
to false unless we know when the instance is closed (it will also always be open after the
first time we open it). We'll need a way for the <InfoWindow /> component to communicate
back with it's parent that the InfoWindow has been closed (either through clicking the x at
the top of the window OR by clicking on the <Map />).
If we click on the map, our <Map /> instance already knows how to handle clicking
callbacks. Let's update the <Container /> component to reset the state of
the this.state.showingInfoWindow:
render() {
const style = {
width: '100vw',
height: '100vh'
}
return (
<div style={style}>
<Map google={this.props.google}
onClick={this.onMapClick}>
{/* ... */}
</Map>
</div>
)
}
}
// ...
Now, if we click on the <Map /> instance, the state of the Container will update
the showingInfoWindow and our <InfoWindow /> instance visibility will be reflected
accordingly.
Finally, we'll need to add a callback to the infoWindow to be called when the infowindow is
opened as well as when it's closed (although for now, we'll only use the callback when it's
closed). To add the callback, we'll need to hook into the state of the infowindow instance.
When we create the infowindow instance in our component, we can attach a few listeners to
the instance to handle the case when each of the events are run:
renderInfoWindow() {
let {map, google, mapCenter} = this.props;
google.maps.event
.addListener(iw, 'closeclick', this.onClose.bind(this))
google.maps.event
.addListener(iw, 'domready', this.onOpen.bind(this));
}
onOpen() {
if (this.props.onOpen) this.props.onOpen();
}
onClose() {
if (this.props.onClose) this.props.onClose();
}
}
Our <InfoWindow /> component can now handle callback actions when it's open or closed.
Let's apply this callback in our <Container /> component to reset the
render() {
const style = {
width: '100vw',
height: '100vh'
}
return (
<div style={style}>
<Map google={this.props.google}
onClick={this.onMapClick}>
{/* ... */}
<InfoWindow
marker={this.state.activeMarker}
visible={this.state.showingInfoWindow}
onClose={this.onInfoWindowClose}>
<div>
<h1>{this.state.selectedPlace.name}</h1>
</div>
</InfoWindow>
</Map>
</div>
)
}
}
// ...
Conclusion
As we built our Google Map component, we've walked through a lot of complex
interactions from parent to children components, interacting with an outside library,
keeping the state of a native JS library in line with a component, and much more.
The entire module is available at npm google-maps-react. Feel free to check it out, pull the
source, contribute back.
If you're stuck, have further questions, feel free to reach out to us by:
Ari Lerner
Hi, I'm Ari. I'm an author of Fullstack React and ng-book and I've been teaching Web
Development for a long time. I like to speak at conferences and eat spicy food.
I technically got paid while I traveled the country as a professional comedian, but have
come to terms with the fact that I am not funny.