tutorial // Jul 23, 2021

How to Render a Map with Markers Using Google Maps in Next.js

How to render a Google Map with markers inside of a React component using Next.js and animating that map based on a marker boundary.

How to Render a Map with Markers Using Google Maps in Next.js

Getting Started

For this tutorial, we're going to use the CheatCode Next.js Boilerplate as a starting point for our work. First, let's clone a copy:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate

Next, we need to install the dependencies for the boilerplate:

Terminal

cd nextjs-boilerplate && npm install

Finally, start up the boilerplate:

Terminal

npm run dev

With that, we're ready to get started.

Adding Google Maps via CDN

Before we implement our map, we're going to need access to the Google Maps JavaScript API. To get access, we're going to use the official Google CDN link for the API:

/pages/_document.js

import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheet } from "styled-components";
import settings from "../settings";

export default class extends Document {
  static async getInitialProps(ctx) { ... }

  render() {
    const { styles } = this.props;

    return (
      <Html lang="en">
        <Head>
          <meta httpEquiv="Content-Type" content="text/html; charset=utf-8" />
          <meta name="application-name" content="App" />
          ...
          <script
            src={`https://maps.googleapis.com/maps/api/js?key=${settings?.googleMaps?.apiKey}&callback=initMap&libraries=&v=weekly`}
            async
          ></script>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Above, in the /pages/_document.js file that's included in the boilerplate, in the <Head></Head> tag, we've pasted in the <script></script> tag that Google recommends for including the Google Maps JavaScript API in a webpage.

Because this file is pretty big, we've condensed some of the other tags in the <Head></Head> tag with .... The spot where you want to place your own <script></script> tag is just before the closing </Head> tag.

Of note, here, we've changed the src attribute on the tag that we get from Google to allow us to use string interpolation so that we can pass our Google Maps API key via our settings file. In the boilerplate we're using, the /settings/index.js file is responsible for automatically loading the contents of the appropriate /settings/settings-<env>.js where the <env> part is equal to the current value of process.env.NODE_ENV, or, the current environment the app is running in (for this tutorial, development or settings-development.js).

If you don't already have a Google Maps API key, learn how to create one here before you continue.

Back in our /pages/_document.js file, we can import settings from /settings/index.js and reference the values in our settings-<env>.js file. Here, we expect that file to contain an object with a googleMaps property and a nested apiKey value, like this:

/settings/settings-development.js

const settings = {
  googleMaps: {
    apiKey: "Paste Your API Key Here",
  },
  graphql: {
    uri: "http://localhost:5001/api/graphql",
  },
  ...
};

export default settings;

With all of that set, now, when we load our app up, we'll have a global google value available which will have a .maps object on it that we'll use to interact with the library.

Setting global map styles

With the Google Maps API loaded up, next, we want to create our app. Real quick before we do, for our demo, we want to add some global CSS styling to our app that will display our map full screen in the app:

/pages/_app.js

...
import { createGlobalStyle } from "styled-components";
...

const GlobalStyle = createGlobalStyle`
  :root {
    ...
  }

  ${pong} /* CSS for /lib/pong.js alerts. */

  body > #__next > .container {
    padding-top: 20px;
    padding-bottom: 20px;
  }

  body.is-map > #__next > .navbar {
    display: none;
  }

  body.is-map > #__next > .container {
    width: 100%;
    max-width: 100%;
    padding: 0 !important;
  }

  ...
`;

class App extends React.Component {
  state = {
    loading: true,
  };

  async componentDidMount() { ... }

  render() { ... }
}

App.propTypes = {
  Component: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
  pageProps: PropTypes.object.isRequired,
};

export default App;

In the string passed to createGlobalStyle (denoted by the backticks ``), we're adding two CSS rules, anticipating a class being applied to our <body></body> tag is-map:

body.is-map > #__next > .navbar {
  display: none;
}

body.is-map > #__next > .container {
  width: 100%;
  max-width: 100%;
  padding: 0 !important;
}

The first rule here is selecting the navbar element included in the boilerplate by default and hiding it from the screen if the <body></body> tag has the .is-map class. The second rule—also targeting the .is-map class—locates the <div className="container"></div> element that wraps the main content of the page further down in the render() function in the file. The styles here force that container to fill the entire width of the page and removes its default padding (ensuring there's no gap on the left and right sides of the map).

Creating our map

Now we're ready to set up our map. Because we're using Next.js in the boilerplate we cloned earlier, we're going to rely on that framework's router that uses the /pages directory to create routes for our app. For our demo, we're going to render our map at http://localhost:5000/map, so we want to create a new folder called map under /pages:

/pages/map/index.js

import React from "react";
import StyledMap from "./index.css";

class Map extends React.Component {
  state = {};

  componentDidMount() {
    document.body.classList.add("is-map");
  }

  componentWillUnmount() {
    document.body.classList.remove("is-map");
  }

  render() {
    return (
      <StyledMap>
        <div id="google-map" />
      </StyledMap>
    );
  }
}

Map.propTypes = {
  // prop: PropTypes.string.isRequired,
};

export default Map;

Here, we're creating a class-based React component—a far easier way to implement Google Maps in React vs. using the function component pattern. Down in the render() method, we render a component <StyledMap></StyledMap> that wraps around an empty <div></div> with the an id of google-map (where we'll render our map).

In the componentDidMount() function, notice that we're setting the is-map class on the <body></body> tag like we hinted at earlier and in the componentWillUnmount() function (called when we move away from the /map page), we make sure to remove the is-map class as this is the only page where we want the styles we apply based on that class name to be used.

Real quick, let's open up that StyledMap component we're importing from ./index.css near the top of our file:

/pages/map/index.css.js

import styled from "styled-components";

export default styled.div`
  #google-map {
    width: 100%;
    height: 100vh;
  }
`;

Very simple. Here, we're using the styled-components library that's included in the Next.js Boilerplate we're using to create a React component that will automatically have some CSS applied to it. Here, we call to the styled.div function included in the library and pass it a string (denoted by the `` backticks here) of CSS that we want applied to a React component that returns a <div></div> tag.

In case that syntax looks weird, the styled.div`` is just shorthand for styled.div(``) (JavaScript allows us to omit the parentheses if the only argument we're passing to the function is a string).

For our styles, we're just telling the <div></div> where we'll inject our Google Map to fill the entire width and height of the screen.

/pages/map/index.js

import React from "react";
import StyledMap from "./index.css";

class Map extends React.Component {
  state = {
    defaultCenter: {
      lat: 36.1774465,
      lng: -86.7042552,
    },
  };

  componentDidMount() {
    document.body.classList.add("is-map");
    this.handleAttachGoogleMap();
  }

  componentWillUnmount() { ... }

  handleAttachGoogleMap = () => {
    const { defaultCenter } = this.state;
    this.map = new google.maps.Map(document.getElementById("google-map"), {
      center: defaultCenter,
      zoom: 10,
    });
  };

  render() {
    return (
      <StyledMap>
        <div id="google-map" />
      </StyledMap>
    );
  }
}

Map.propTypes = {
  // prop: PropTypes.string.isRequired,
};

export default Map;

Next, in our componentDidMount(), we've added a call to a new function handleAttachGoogleMap() where we've added the important part: a call to new google.maps.Map() passing in a call to document.getElementById('google-map') as the first argument and then a JavaScript object with some settings for our map.

This is saying "select the <div id="google-map" /> element down in our render() function and render the Google Map in that spot." For the options, we set the center property (where the center of the map will be positioned when it loads) to some coordinates that we've set up in the state value under defaultCenter. Notice that Google expects us to pass coordinates as latitude and longitude pairs via JavaScript objects with lat and lng as properties containing those values.

For the zoom we set this to level 10 (the higher the zoom value, the closer we get to street level, the lower the zoom value, the further we're zoomed out). Finally, we assign the result of calling new google.maps.Map() to this.map. What this helps us to accomplish is making our Google Maps instance accessible to our entire component. This will come in handy next when we look at adding markers to our map.

Adding markers to our map

Now that we have access to a Google Maps instance, we can add some markers to the map. To speed things up, we're going to add an array of markers to the default state value near the top of our component with some places near our defaultCenter (you can change these to fit the needs of your own map):

/pages/map/index.js

import React from "react";
import StyledMap from "./index.css";

class Map extends React.Component {
  state = {
    defaultCenter: {
      lat: 36.1774465,
      lng: -86.7042552,
    },
    markers: [
      {
        lat: 36.157055,
        lng: -86.7696144,
      },
      {
        lat: 36.1521981,
        lng: -86.7801724,
      },
      {
        lat: 36.1577547,
        lng: -86.7785841,
      },
      {
        lat: 36.1400674,
        lng: -86.8382887,
      },
      {
        lat: 36.1059131,
        lng: -86.7906082,
      },
    ],
  };

  componentDidMount() { ... }

  componentWillUnmount() { ... }

  handleAttachGoogleMap = () => {
    const { defaultCenter } = this.state;
    this.map = new google.maps.Map(...);

    setTimeout(() => {
      this.handleDrawMarkers();
    }, 2000);
  };

  handleDrawMarkers = () => {
    const { markers } = this.state;
    markers.forEach((marker) => {
      new google.maps.Marker({
        position: marker,
        map: this.map,
      });
    });
  };

  render() { ... }
}

Map.propTypes = {
  // prop: PropTypes.string.isRequired,
};

export default Map;

Inside of handleAttachGoogleMap, after we've created our map instance, now, we're adding a call to this.handleDrawMarkers(), a function we're adding where we'll render the markers for our map. Of note, to make our demo more polished, we're wrapping a setTimeout() for two seconds to say "load the map and then after two seconds, draw the markers." This makes the loading experience more visually interesting for users (though, it's not required so feel free to remove it).

Inside of handleDrawMarkers(), we use JavaScript destructuring to "pluck off" the markers value that we've added to state (again, just an array of latitude/longitude objects). Using the JavScript .forEach() method on our markers array, we loop over the list and for each one, call to new google.maps.Markers(). To that function, we we pass an options object describing the position for our marker (the latitude/longitude pair) and the map we want to add the marker to (our existing Google Map instance we stored at this.map).

Though it may not look like much, when we load up our page, we should see our map rendered and, after a two-second delay, our markers appear.

gUg5LCJQojYJm1ij/Elg5ikqyLL3RcKKt.0
Our Google Map with our list of markers rendered.

We're not quite done, though. To wrap up, we're going to polish things up by using the Google Maps bounds feature to clean up the user experience.

Using markers as map bounds to set center and zoom

All of the work we need to do now is going to be in the handleDrawMarkers() function:

/pages/maps/index.js

handleDrawMarkers = () => {
  const { markers } = this.state;
  const bounds = new google.maps.LatLngBounds();

  markers.forEach((marker) => {
    new google.maps.Marker({
      position: marker,
      map: this.map,
    });

    bounds.extend(marker);
  });

  this.map.fitBounds(bounds);
  this.map.panToBounds(bounds);
};

Focusing just on that function, now, we want to use the .LatLngBounds() method in the google.maps library to help us set a boundary around our markers on the map. To do it, we've added a line above our .forEach(), creating an instance of google.maps.LatLngBounds(), storing it in a variable const bounds.

Next, inside of our markers.forEach(), after we've created our marker, we add a call to bounds.extend(), passing in our marker (our latitude/longitude pair). This function "pushes out" the boundary we initialized in bounds to include the marker we're currently looping over (think of this like pushing out pizza dough in a circle on your counter where the center of the pizza is where our markers are located).

Beneath our .forEach() loop, we next call to two functions on our this.map instance: .fitBounds() which takes in the bounds we've built up and "shrinkwraps" the map to that boundary (zooming in) and .panToBounds(), moves the center of the map to be the center of the boundary we just drew.

With this, now, when our map loads, we'll see a nice animation as our markers are added to the map.

Wrapping up

In this tutorial, we learned how to add Google Maps to a Next.js app and render a map in a React.js component, complete with markers and an animated zoom effect based on the boundary of those markers.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode