0% found this document useful (0 votes)
95 views

How To Write A Google Maps React Component

This document provides instructions for building a React component that integrates the Google Maps API. It discusses loading the Google API asynchronously, creating a Map component that loads the map when the API is ready, and handling updates when new props are received. It also covers adding interactivity to the map by dragging it, clicking markers, and displaying info windows. The component examples allow building complex nested React components that utilize third party libraries like Google Maps.

Uploaded by

d caars
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
95 views

How To Write A Google Maps React Component

This document provides instructions for building a React component that integrates the Google Maps API. It discusses loading the Google API asynchronously, creating a Map component that loads the map when the API is ready, and handling updates when new props are received. It also covers adding interactivity to the map by dragging it, clicking markers, and displaying info windows. The component examples allow building complex nested React components that utilize third party libraries like Google Maps.

Uploaded by

d caars
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 45

How to Write a Google Maps

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.

 Try the live demo here


 The completed code can be found on Github here
Table of Contents

1. Loading a Google-based Component


2. Adding props to the Map Component
3. Adding state to the Map Component
4. Using the Browser's Current Location
5. Dragging the Map Around with addListener
6. Adding Markers to the Map
7. Creating the MarkerComponent
8. Adding a Marker Info Window
9. Conclusion

In this post, we'll look at how we to connect the Google API and build a Google Maps
Component.
Demo

Map data ©2019 Google


Terms of Use

Report a map error

This page can't load Google Maps correctly.

Do you own this website? OK

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.

Our apiKey is: AIzaSyAyesbQMyKVVbBgKVi2g6VX7mop2z96jBo

Loading a Google-based
Component
In order to use Google within our components, we'll need to handle two technical
boundaries:

1. Loading the Google API


2. Handling access to the Google API within our components.
Our goal here is to create an independent component that can handle these two tasks for
us. Let's build a GoogleApiComponent to handle taking care of this for us (alternatively,
we've wrapped this into an npm module ( google-maps-react). Feel free to grab this npm
module and head to the next section).

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.

First, grab the ScriptCache.js script from this gist.

There are 3 scripts included in the gist. The scripts:

 ScriptCache.js - The backbone of this method which asynchronously loads


JavaScript <script> tags on a page. It will only load a single <script> tag on a page
per-script tag declaration. If it's already loaded on a page, it calls the callback from
the onLoad event immediately.

Sample usage:

this.scriptCache = cache({
google: 'https://api.google.com/some/script.js'
});

 GoogleApi.js is a script tag compiler. Essentially, this utility module builds a


Google Script tag link allowing us to describe the pieces of the Google API we
want to load in using a JS object and letting it build the endpoint string.

Sample usage:

GoogleApi({
apiKey: apiKey,
libraries: ['places']
});

 - The React wrapper which is responsible for loading a


GoogleApiComponent.js
component and passing through the window.google object after it's loaded on the
page.

Sample usage:

const Container = React.createClass({


render: function() {
return <div>Google</div>;
}
})
export default GoogleApiComponent({
apiKey: __GAPI_KEY__
})(Container)

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 Map Container Component


Before we jump into building our Map component, let's build our container component to
demonstrate usage as well as be responsible for loading the Google Api:

export class Container extends React.Component {


render() {
if (!this.props.loaded) {
return <div>Loading...</div>
}
return (
<div>Map will go here</div>
)
}
}

export default GoogleApiComponent({


apiKey: __GAPI_KEY__
})(Container)

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:

export class Container extends React.Component {


render() {
return (
<div>
<Map google={this.props.google} />
</div>
)
}
}
// ...

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:

export class Container extends React.Component {


render() {
const style = {
width: '100vw',
height: '100vh'
}
return (
<div style={style}>
<Map google={this.props.google}
/>
</div>
)
}
}
// ...

The Map Component


With the stage set for our Container component, let's start our Map component.
Our Map component is essentially a simple wrapper around the default Google Maps api.
The tricky part about using the asynchronous library is being able to depend upon it's API
being available.

Let's build the basic Map component:

export class Map extends React.Component {


render() {
return (
<div ref='map'>
Loading map...
</div>
)
}
}

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.

export class Map extends React.Component {


componentDidUpdate(prevProps, prevState) {
if (prevProps.google !== this.props.google) {
this.loadMap();
}
}

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.

export class Map extends React.Component {


componentDidMount() {
this.loadMap();
}

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:

export class Map extends React.Component {


loadMap() {
if (this.props && this.props.google) {
// google is available
const {google} = this.props;
const maps = google.maps;
}
// ...
}
}

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:

export class Map extends React.Component {


loadMap() {
if (this.props && this.props.google) {
// google is available
const {google} = this.props;
const maps = google.maps;

const mapRef = this.refs.map;


const node = ReactDOM.findDOMNode(mapRef);
}
// ...
}
}

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.

export class Map extends React.Component {


loadMap() {
if (this.props && this.props.google) {
// google is available
const {google} = this.props;
const maps = google.maps;

const mapRef = this.refs.map;


const node = ReactDOM.findDOMNode(mapRef);

let zoom = 14;


let lat = 37.774929;
let lng = -122.419416;
const center = new maps.LatLng(lat, lng);
const mapConfig = Object.assign({}, {
center: center,
zoom: zoom
})
this.map = new maps.Map(node, mapConfig);
}
// ...
}
}

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:

 center - the combination of latitude and longitude to display (in


a map.LatLng() object)
 zoom - the level of zoom to display, i.e. how close to the center we should display.

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

Map data ©2019 Google


Terms of Use

Report a map error


This page can't load Google Maps correctly.

Do you own this website? OK

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

Defining propTypes on a component is always good practice to both


document our components and make them more easily sharable. For more
information on documenting propTypes, the React documentation is a
convincing place to read more.
export class Map extends React.Component {
// ...
}
Map.propTypes = {
google: React.PropTypes.object,
zoom: React.PropTypes.number,
initialCenter: React.PropTypes.object
}

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:

export class Map extends React.Component {


// ...
}
Map.propTypes = {
google: React.PropTypes.object,
zoom: React.PropTypes.number,
initialCenter: React.PropTypes.object
}
Map.defaultProps = {
zoom: 13,
// San Francisco, by default
initialCenter: {
lat: 37.774929,
lng: -122.419416
}
}

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:

export class Map extends React.Component {


loadMap() {
if (this.props && this.props.google) {
// google is available
const {google} = this.props;
const maps = google.maps;

const mapRef = this.refs.map;


const node = ReactDOM.findDOMNode(mapRef);

let {initialCenter, zoom} = this.props;


const {lat, lng} = initialCenter;
const center = new maps.LatLng(lat, lng);
const mapConfig = Object.assign({}, {
center: center,
zoom: zoom
})
this.map = new maps.Map(node, mapConfig);
}
// ...
}
}

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.

Let's go ahead and make the map stateful:


export class Map extends React.Component {
constructor(props) {
super(props);

const {lat, lng} = this.props.initialCenter;


this.state = {
currentLocation: {
lat: lat,
lng: lng
}
}
}
// ...
}

We can update the loadMap() function to pull from the state, rather than from props:

export class Map extends React.Component {


loadMap() {
if (this.props && this.props.google) {
// ...
const {lat, lng} = this.state.currentLocation;
}
// ...
}
}

Demo
Go

Lat:
Lng:

Map data ©2019 Google


Terms of Use

Report a map error

This page can't load Google Maps correctly.

Do you own this website? OK

Using the Browser's Current


Location
Wouldn't it be more exciting if we could use the browser's technology to determine the
current location of the viewer instead of hardcoding the lat and lng props?

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.

First, let's set the prop:

export class Map extends React.Component {


// ...
}
Map.propTypes = {
google: React.PropTypes.object,
zoom: React.PropTypes.number,
initialCenter: React.PropTypes.object,
centerAroundCurrentLocation: React.PropTypes.bool
}
Map.defaultProps = {
zoom: 13,
// San Francisco, by default
initialCenter: {
lat: 37.774929,
lng: -122.419416
},
centerAroundCurrentLocation: false
}

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:

export class Map extends React.Component {


// ...
componentDidMount() {
if (this.props.centerAroundCurrentLocation) {
if (navigator && navigator.geolocation) {
navigator.geolocation.getCurrentPosition((pos) => {
const coords = pos.coords;
this.setState({
currentLocation: {
lat: coords.latitude,
lng: coords.longitude
}
})
})
}
}
this.loadMap();
}
// ...

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.

export class Map extends React.Component {


componentDidUpdate(prevProps, prevState) {
if (prevProps.google !== this.props.google) {
this.loadMap();
}
if (prevState.currentLocation !== this.state.currentLocation) {
this.recenterMap();
}
}

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:

export class Map extends React.Component {


recenterMap() {
const map = this.map;
const curr = this.state.currentLocation;

const google = this.props.google;


const maps = google.maps;

if (map) {
let center = new maps.LatLng(curr.lat, curr.lng)
map.panTo(center)
}
}
// ...
}

Demo

Map data ©2019 Google


Terms of Use

Report a map error


This page can't load Google Maps correctly.

Do you own this website? OK

Dragging the Map Around


with addListener
Since we have our Map component set, the we can interact with it in a lot of ways. The
google map api is rich with opportunities for handling events that happen within the map
(just check out the extensive documentation). We can set up callbacks to call when these
events occur within the map instance itself.

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.

export class Map extends React.Component {


loadMap() {
if (this.props && this.props.google) {
// ...
this.map = new maps.Map(node, mapConfig);

this.map.addListener('dragend', (evt) => {


this.props.onMove(this.map);
})
}
// ...
}
}
Map.propTypes = {
// ...
onMove: React.PropTypes.func
}
Map.defaultProps = {
onMove: function() {} // default prop
}

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.

export class Map extends React.Component {


loadMap() {
if (this.props && this.props.google) {
// ...
this.map = new maps.Map(node, mapConfig);

let centerChangedTimeout;
this.map.addListener('dragend', (evt) => {
if (centerChangedTimeout) {
clearTimeout(centerChangedTimeout);
centerChangedTimeout = null;
}
centerChangedTimeout = setTimeout(() => {
this.props.onMove(this.map);
}, 0);
})
}
// ...
}
}

Handling More Events


Although we are only handling the dragend event above, we can handle other events as
well in a similar fashion, but this can get really cumbersome, really fast. We can be a
little bit more clever and more programatic about building our interactivity into our
component.

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:

const evtNames = ['click', 'dragend'];

With our evtNames list, let's replace our addListener() funcitonality from above with a loop
for each of the evtNames:

export class Map extends React.Component {


loadMap() {
if (this.props && this.props.google) {
// ...
this.map = new maps.Map(node, mapConfig);

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:

export class Map extends React.Component {


handleEvent(evtName) {
let timeout;
return (e) => {
// ...
}
}
}

We'll basically copy+paste our timeout functionality into our new handleEvent() function.

export class Map extends React.Component {


handleEvent(evtName) {
let timeout;
const handlerName = evtName;
return (e) => {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
timeout = setTimeout(() => {
if (this.props[handlerName]) {
this.props[handlerName](this.props, this.map, e);
}
}, 0);
}
}
}

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.

A simple camelize() helper function might look something similar to:

const camelize = function(str) {


return str.split(' ').map(function(word){
return word.charAt(0).toUpperCase() + word.slice(1);
}).join('');
}
camelize('i love you'); // ILoveYou
camelize('say hello'); // SayHello

With our camelize() helper function, we can replace the handlerName from
our handleEvent function:

export class Map extends React.Component {


handleEvent(evtName) {
let timeout;
const handlerName = `on${camelize(evtName)}`;

return (e) => {


if (timeout) {
clearTimeout(timeout);
timeout = null;
}
timeout = setTimeout(() => {
if (this.props[handlerName]) {
this.props[handlerName](this.props, this.map, e);
}
}, 0);
}
}
}

Lastly, because we are good React-citizens, let's add these properties to our propTypes:

evtNames.forEach(e => Map.propTypes[camelize(e)] = T.func)

Handling Custom Events on Map


We can also fire our own custom events along with the google map instance. Allowing us
to listen for our own custom events is an incredibly useful feature that gives us the ability
to react to custom functionality using the same event handling mechanism we just set up.

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):

const evtNames = ['ready', 'click', 'dragend'];

To trigger an event, like the ready event we can use


the google.maps.event object's trigger() function.

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.

export class Map extends React.Component {


loadMap() {
if (this.props && this.props.google) {
// ...
this.map = new maps.Map(node, mapConfig);

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.

Adding Markers to the Map


What good is a Google Map without markers indicating location spots on the map, eh?
Let's add a method for our users to place a marker on our map. We could set up
our Map component to accept a list of places and be responsible for setting up the markers
itself, or we can build the Map component in the React Way and build custom components
to manipulate the calendar as children.

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.

export class Container extends React.Component {


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 />
<Marker position={pos} />
</Map>
</div>
)
}
}
// ...

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:

export class Map extends React.Component {


renderChildren() {
// ...
}

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:

export class Map extends React.Component {


renderChildren() {
const {children} = this.props;
if (!children) return;
}
// ...
}

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:

export class Map extends React.Component {


renderChildren() {
const {children} = this.props;

if (!children) return;

return React.Children.map(children, c => {


return React.cloneElement(c, {
map: this.map,
google: this.props.google,
mapCenter: this.state.currentLocation
});
})
}
// ...
}

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:

Creating the MarkerComponent


The google api for markers requires that we have at least a position defined on it and it
looks like:

let marker = new google.maps.Marker({


position: somePosition,
map: map
})

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).

export class Marker extends React.Component {


render() {
return null;
}
}

While we are at it, let's also define our propTypes for our Marker component. We'll need to
define a position at minimum.

export class Marker extends React.Component {


render() {
return null;
}
}

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.

export class Marker extends React.Component {


componentDidUpdate(prevProps) {
// component updated
}
renderMarker() {
// ...
}
// ...
}

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:

export class Marker extends React.Component {


componentDidUpdate(prevProps) {
if ((this.props.map !== prevProps.map) ||
(this.props.position !== prevProps.position)) {
// The relevant props have changed
}
}
renderMarker() {
// ...
}
// ...
}

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:

export class Marker extends React.Component {


componentDidUpdate(prevProps) {
if ((this.props.map !== prevProps.map) ||
(this.props.position !== prevProps.position)) {
this.renderMarker();
}
}
renderMarker() {
let {
map, google, position, mapCenter
} = this.props;

let pos = position || mapCenter;


position = new google.maps.LatLng(pos.lat, pos.lng);
}
// ...
}

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;

let pos = position || mapCenter;


position = new google.maps.LatLng(pos.lat, pos.lng);

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:

const evtNames = ['click', 'mouseover'];

Back when we create the Marker instance, we can add functionality to handle the event:

export class Marker extends React.Component {


renderMarker() {
// ...
this.marker = new google.maps.Marker(pref);

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:

export class Marker extends React.Component {


handleEvent(evtName) {
return (e) => {
const evtName = `on${camelize(evt)}`
if (this.props[evtName]) {
this.props[evtName](this.props, this.marker, e);
}
}
}
}

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

Report a map error


This page can't load Google Maps correctly.

Do you own this website? OK

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:

export class Marker extends React.Component {


componentWillUnmount() {
if (this.marker) {
this.marker.setMap(null);
}
}
}

Adding a Marker Info Window


From here, we can use the marker as a point of reference for our user to click on to get
more information about each of the markers. In the Google API, the window that pops up
over each of the markers is called an InfoWindow. To create an InfoWindow, we must
pass in a string of html to show. Since the InfoWindow itself isn't a React component, we'll
need to handle the conversion from a React component to html.

First, let's look at the usage of a ne InfoWindow component. In


our Container.render() function, let's add a reference to the new component we'll create.
The InfoWindow Google instance will require a marker to determine where to place the
element, we'll need to pass one in. We'll also programmatically handle showing/hiding
the InfoWindow component so we can operate with it in the React Way.

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:

export class Container extends React.Component {


onMarkerClick: function(props, marker, e) {
this.setState({
selectedPlace: props,
activeMarker: marker,
showingInfoWindow: true
});
},
}

We'll handle updating the state of the component when we click on the <Marker /> above

Making an InfoWindow Component


Let's start our our InfoWindow component as usual. It will be similar to
the Marker component in that we're not going to use the virtual dom component it creates.

class InfoWindow extends React.Component {


render() {
return null;
}
}

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.

class InfoWindow extends React.Component {


componentDidUpdate(prevProps) {
// ...
}
renderInfoWindow() {
}
}

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.

Let's handle each one at a time.

1. Map instance has become available


The first case is the opportunity for us to create the google.maps.InfoWindow() instance.
We'll use the google api to create the instance when a map instance is available. Here is
where we can set the this.infowindow instance on the component:

class InfoWindow extends React.Component {


componentDidUpdate(prevProps, prevState) {
if (this.props.map !== prevProps.map) {
this.renderInfoWindow();
}
}
renderInfoWindow() {
let {map, google, mapCenter} = this.props;

const iw = this.infowindow = new google.maps.InfoWindow({


content: ''
});
}
}

We're keeping a reference to the this.infowindow with a const iw in


the componentDidUpdate() function. We'll come back to using this instance
shortly.

2. The content of the InfoWindow has been updated


Finally, if the content of the InfoWindow has been updated and the visibility has not been
updated, then we can update the content of the infowindow.

class InfoWindow extends React.Component {


componentDidUpdate(prevProps, prevState) {
if (this.props.map !== prevProps.map) {
// ...
}

if (this.props.children !== prevProps.children) {


this.updateContent();
}
}

updateContent() {}
}

The updateContent() method from above is a simple wrapper around the


google InfoWindow instance to call setContent():
class InfoWindow extends React.Component {
updateContent() {
const content = this.renderChildren();
this.infowindow
.setContent(content);
}

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 get a hold of the ReactDOMServer from the react-dom package:

import ReactDOMServer from 'react-dom/server'

We can use this package to translate the children of the <InfoWindow /> component in
our renderChildren() function:

class InfoWindow extends React.Component {


renderChildren() {
const {children} = this.props;
return ReactDOMServer.renderToString(children);
}
}

3. The visibility of the InfoWindow has changed


If the visible prop has changed, we'll want to show or hide the InfoWindow component.
Since this requires a programatic update, we'll need to check the value of the prop and
open the window if it's visible or close it if it's not available.

As we're checking against the previous props, we know that the infoWindow is closed if
the visible prop is true and visa versa.

class InfoWindow extends React.Component {


componentDidUpdate(prevProps, prevState) {
if (this.props.map !== prevProps.map) {
// ...
}

if (this.props.visible !== prevProps.visible) {


this.props.visible ?
this.openWindow() :
this.closeWindow();
}
}
}

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:

class InfoWindow extends React.Component {


componentDidUpdate(prevProps) {
if (this.props.map !== prevProps.map) {
// ...
}

if ((this.props.visible !== prevProps.visible) ||


(this.props.marker !== prevProps.marker)) {
this.props.visible ?
this.openWindow() :
this.closeWindow();
}
}

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:

class InfoWindow extends React.Component {


openWindow() {
this.infowindow
.open(this.props.map, this.props.marker);
}
closeWindow() {
this.infowindow.close();
}
}

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

Report a map error

This page can't load Google Maps correctly.

Do you own this website? OK

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:

export class Container extends React.Component {


onMapClick() {
if (this.state.showingInfoWindow) {
this.setState({
showingInfoWindow: false,
activeMarker: null
});
}
},

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:

class InfoWindow extends React.Component {


componentDidUpdate(prevProps, prevState) {
if (this.props.map !== prevProps.map) {
this.renderInfoWindow();
}
}

renderInfoWindow() {
let {map, google, mapCenter} = this.props;

const iw = this.infowindow = new google.maps.InfoWindow({


content: ''
});

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

export class Container extends React.Component {


onInfoWindowClose: function() {
this.setState({
showingInfoWindow: false,
activeMarker: null
})
},

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:

 Commenting on this post at the end of the article


 Emailing us at [email protected]
 Tweet at us at @fullstackreact
 Filling an issue on the Github repo
LEARN REACT THE RIGHT WAY
The up-to-date, in-depth, complete guide to React and friends.

DOWNLOAD THE FIRST CHAPTER

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.

Connect with Ari on Twitter at @auser.

You might also like