alvinashcraft
shared this story
.
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.
Table of Contents
Loading a Google-based Component
Adding props to the Map Component
Adding state to the Map Component
Using the Browser’s Current Location
Dragging the Map Around with addListener
Adding Markers to the Map
Creating the MarkerComponent
Adding a Marker Info Window
Conclusion
In this post, we’ll look at how we to connect the Google API and build a Google Maps Component.
Map Data
Map data ©2016 Google
Before we can integrate with Google Maps, we’ll need to sign up at Google to get an API key.
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:
Loading the Google API
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:
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 inusing a JS object and letting it build the endpoint string.
Sample usage:
GoogleApiComponent.js - The React wrapper which is responsible for loading a component and passing through the window.google object after it’s loaded on the page.
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 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:
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:
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:
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.
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.
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:
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.
Map Data
Map data ©2016 Google
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.
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.
Let’s go ahead and make the map stateful:
We can update the loadMap() function to pull from the state, rather than from props:
Map Data
Map data ©2016 Google
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:
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.
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:
Map Data
Map data ©2016 Google
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.
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.
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:
With our evtNames list, let’s replace our addListener() funcitonality from above with a loop for each of the evtNames:
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.
A simple camelize() helper function might look something similar to:
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:
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):
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.
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.
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:
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:
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:
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.
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:
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:
Our handleEvent() function will look nearly the same as the function in the <Map /> component:
San Francisco
Edwardian Hotel
Travelodge San Francisco Central
Hayes Valley Inn
SOM