I've set up Google Maps JavaScript API for my Next.js app. The API documentation on how to get started (Google 2021) is very well-written, but I encountered a few gotchas when the API is used together with Next.js and also ESLint. Let me take note of them below for your information (and for my future self).
Updates on July 31, 2021: A new section entitled "Gotcha #4: API Key" is added. Consequently, the title of this article is changed from "3 gotchas when setting up Google Maps API with Next.js and ESLint".
Updates on August 15, 2021: A new section entitled "Bonus: Remove all the default buttons" is added at the end of the article.
Updates on Sep. 3, 2021: Change the link to Google's documentation on restricting URLs that can send API requests. Google Cloud's documentation is better written than Google Map Platform's on this matter.
Gotcha #1: CSS
TL;DR
Add the following CSS declaration:
#__next {
height: 100%;
}
Detail
To show a Google Map across the entire browser window, Google (2021) recommends the following CSS code:
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
#map {
height: 100%;
}
where #map
is the id
for the container element in which a Google Map will be shown.
With Next.js, however, the #map
container will not be a direct child of the body
element. There will be another div
with #__next
as its id
attribute. In other words, Next.js will compile your React code into the following HTML code:
<html>
<body>
<div id="__next">
<div id="map"></div>
</div>
</body>
</html>
By default, the #__next
container has height:auto
. As it doesn't recognize any content, the height will be zero. So the following CSS declaration
#map {
height: 100%;
}
will set the height of the #map
container to be 100% of zero. That is, zero. As a result, a Google Map inside the container won't be shown.
A workaround is suggested by SkyzohKey (2018):
#__next {
height: 100%;
}
This will ensure that the #__next
container's height will be 100% of the body
element's height, which is in turn 100% of the html
element's height, which is in turn 100% of the browser window height.
- Incidentally, I haven't found any documentation saying the
height:100%
will refer to the browser window height when it's applied to thehtml
element. Let me know if you know where to look for.
Consequently, the #map
container's height will be 100% of the #__next
container, that is, the browser window height.
Gotcha #2: React hooks
TL;DR
Compose the pages/index.js
as follows:
// pages/index.js
import {useEffect, useRef} from 'react';
import {Loader} from '@googlemaps/js-api-loader';
function HomePage() {
const googlemap = useRef(null);
useEffect(() => {
const loader = new Loader({
apiKey: 'yourAPIkey',
version: 'weekly',
});
let map;
loader.load().then(() => {
map = new google.maps.Map(googlemap.current, {
center: {lat: -34.397, lng: 150.644},
zoom: 8,
});
});
});
return (
<div id="map" ref={googlemap} />
);
}
export default HomePage;
Detail
Google (2021) suggests the following JavaScript code to embed a Google Map:
map = new google.maps.Map(document.getElementById("map"), {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
});
where the #map
container is referenced with document.getElementById("map")
. An experienced React user can immediately tell that this should be replaced with the useRef
hook.
- For why we should use
useRef()
instead ofdocument.getElementById()
, see Farmer (2018).
In addition, when we need to refer to the element during the initial rendering of a React component, we should use the useEffect
hook. So all the JavaScript code to embed a Google Map needs to be written inside the useEffect
hook block.
This is a technique I've learned for using the canvas
element with React. See Kudamatsu (2020) (see Step 4) for detail.
Gotcha #3: Handling ESLint error
TL;DR
Add the following line immediately before creating a map instance:
const google = window.google;
Detail
The code in the previous two sections will do render a Google Map. But if you use ESLint, it throws an error because of this line:
map = new google.maps.Map(googlemap.current, {...});
The object called google
is used without being defined. ESLint doesn't like it. And it is a compile error. So you cannot tell ESLint to ignore this line of code (ESLint 2019).
A workaround is suggested by Abramov (2017). He explains why ESLint complains:
"... people commonly misunderstand the difference between local variables, imported modules, and global variables, and so we want to always make it clear in the code when you use a global variable."
So to make it clear that google
is a global variable, we should write the useEffect
block of code in the following way:
useEffect(() => {
const loader = new Loader({
apiKey: 'yourAPIkey',
version: 'weekly',
});
let map;
loader.load().then(() => {
const google = window.google; // ADDED
map = new google.maps.Map(googlemap.current, {
center: {lat: -34.397, lng: 150.644},
zoom: 8,
});
});
});
The window.google
is undefined
until the Google API library is referenced (Marcus 2018). So it has to be inside the loader.load().then()
block.
Gotcha #4: API key
Embarrassingly, when I first used Google Maps API, I hard-coded its API key, committed it with Git, and pushed it to the GitHub repo. Google immediately emailed me with a message, urging me to change the API key as soon as possible.
Since then, I learned about how to secure API keys for a back-end server by saving them as environment variables defined in the .env
file (which needs to be git-ignored) with the help of the dotenv
library (see Sanatan 2019 for detail).
This standard technique, however, cannot directly be applied to Google Maps API, which requires browsers, not back-end servers, to access to the API key. Plus, Next.js has its own complication when it comes to the use of environment variables.
I've figured out that there are two approaches to handle API keys when we use Google Maps API with Next.js.
Approach 1: Next.js built-in environment variables
Step 1: Create a file called .env.local
in the root directory of a project. Next.js will automatically load environment variables in .env.local
into process.env
. For detail, see Next.js documentation.
Step 2: Add your API key to the .env.local
file in the following way:
NEXT_PUBLIC_API_KEY=ROCHjzuh5szlxhgjh2duYDHjdg
where a random series of characters to the right hand of =
needs to be replaced with your own API key for Google Maps. The variable name to the left of =
has to start with NEXT_PUBLIC_
, followed by any name of your choice. Otherwise, browsers cannot access to its value. For detail, see Next.js documentation.
Step 3: Add .env.local
to .gitignore
so that your API key won't be committed to your Git repo.
Step 4: In the useEffect
hook (see the "Gotcha #2: React hooks" section above), refer to the API key as process.env.NEXT_PUBLIC_API_KEY
:
useEffect(() => {
const loader = new Loader({
apiKey: process.env.NEXT_PUBLIC_API_KEY,
version: 'weekly',
});
let map;
loader.load().then(() => {
...
})
})
That's all!
But you may not like this NEXT_PUBLIC_
prefix. Also you may want to use .env
, not .env.local
, as the file name for environment variables. If so, there is an alternative approach.
Approach 2: dotenv
This approach is a technique that I learned from Surya (2021).
Step 1: Create a file called .env
. (The file doesn't have to be in the root directory; see Step 5 below.)
Step 2: Add your API key to the .env
file as follows:
API_KEY=ROCHjzuh5szlxhgjh2duYDHjdg
where a random series of characters to the right hand of =
needs to be replaced with your own API key for Google Maps. Change API_KEY
to some other name, if you wish. You don't have to prefix the variable name with NEXT_PUBLIC_
.
Step 3: Add .env
to .gitignore
.
Step 4: Install dotenv
with
npm install dotenv
Step 5: Configure dotenv
in next.config.js
(the Next.js configuration file) as follows:
const webpack = require('webpack');
const {parsed: myEnv} = require('dotenv').config();
module.exports = {
webpack(config) {
config.plugins.push(new webpack.EnvironmentPlugin(myEnv));
return config;
},
};
If you need to save .env
somewhere else than the root directory of your project, say, /src/.env
, then change the second line to:
const {parsed: myEnv} = require('dotenv').config({
path:'/src/.env'
});
Step 6: In the useEffect
hook, refer to the API key as process.env.API_KEY
:
useEffect(() => {
const loader = new Loader({
apiKey: process.env.API_KEY,
version: 'weekly',
});
let map;
loader.load().then(() => {
...
})
That's it.
This approach requires an extra package and an extra configuration. In return, you gain more freedom in the naming of the environment variable for your API key and where to save the .env
file.
Security measures
Either of the above two approaches will expose your API keys in the Network tab of Chrome DevTools. As far as I understand, this is unavoidable because Google Maps API won't allow a map to be rendered with a server. Browsers need to make a request to Google Maps's server with your API key as part of a query string.
So Google Maps API documentation recommends restricting the URLs from which a request to Google Maps's server is made with your API key. If someone steals your API key, then, they won't be able to use it from their own web app.
For how to restrict URLs, see Google Cloud documentation on using API keys.
Summary
Your pages/index.js
should look like this:
// pages/index.js
import {useEffect, useRef} from 'react';
import {Loader} from '@googlemaps/js-api-loader';
function HomePage() {
const googlemap = useRef(null);
useEffect(() => {
const loader = new Loader({
apiKey: process.env.NEXT_PUBLIC_API_KEY,
version: 'weekly',
});
let map;
loader.load().then(() => {
const google = window.google;
map = new google.maps.Map(googlemap.current, {
center: {lat: -34.397, lng: 150.644},
zoom: 8,
});
});
});
return (
<div id="map" ref={googlemap} />
);
}
export default HomePage;
Then add the following CSS declarations:
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
#__next {
height: 100%;
}
#map {
height: 100%;
}
Finally, save your API key as NEXT_PUBLIC_API_KEY
in the .env.local
file (which needs to be git-ignored) in the root directory. (See the subsection entitled "Approach 2" above if you want to avoid prefixing the environment variable for your API key with NEXT_PUBLIC_
and/or to save it in .env
.)
Bonus: Remove all the default buttons
This is not "gotcha" due to the use of Next.js. But it's likely to be what interests those who have read this article this far.
By default, Google Maps will show several buttons: on the top left, there are the tabs to switch the map style to the satellite image; on the top right, the button to enter the full-screen mode; on the bottom right, the "pegman" to enter the street view mode and the zoom in/out buttons.
How Google Maps API renders a map with the default settings (image source: a screenshot taken by the author)
If you don't need them all, edit the map
variable as follows:
map = new google.maps.Map(googlemap.current, {
center: {lat: -34.397, lng: 150.644},
zoom: 8,
fullscreenControl: false, // remove the top-right button
mapTypeControl: false, // remove the top-left buttons
streetViewControl: false, // remove the pegman
zoomControl: false, // remove the bottom-right buttons
});
For the documentation on these and other options, see the "Map Options interface" section of Google Maps JavaScript API V3 Reference.
Hope this article helps you kick-start the use of Google Maps API in your Next.js project!
And maybe you're also interested in the following articles of mine about more advanced uses of Google Maps API:
Day 4: Customizing Google Maps color scheme (and its place label visibility)
Masa Kudamatsu ・ Sep 3 '21
Day 6: Using Google Fonts as Google Maps place markers
Masa Kudamatsu ・ Sep 16 '21
Day 12: Showing user location on embedded Google Maps (with Geolocation API and React)
Masa Kudamatsu ・ Oct 26 '21
References
Dan Abramov (2017) “An answer to ‘google is not defined in react app using create-react-app’”, Stack Overflow, May 1, 2017.
ESLint (2019) “Disabling Rules with Inline Comments”, ESLint User Guide, Dec. 1, 2019.
Farmer, Andrew H. (2018) “Why to use refs instead of IDs”, JavaScript Stuff, Jan 27, 2018.
Google (2021) “Overview”, Maps JavaScript API Guides, Feb. 10, 2021.
Kudamatsu, Masa (2020) “How to use HTML Canvas with React Hooks — Web Dev Survey from Kyoto”, medium.com, Dec. 9, 2020.
Marcus, Scott (2018) “A comment to ‘window.google is undefined in react?’”, Stack Overflow, Apr. 25, 2018.
Sanatan, Marcus (2019) "Managing Environment Variables in Node.js with dotenv", Stack Abuse, last updated on May 15, 2019.
SkyzohKey (2018) “An answer to ‘Nextjs:How to change css of root div __next on specific page?’”, Stack Overflow, Dec. 5, 2018.
Surya, Deepak (2021) "Environmental Variables in Next.js with dotenv", Courtly & Intrepid, Feb. 3, 2021.
Top comments (3)
Hey Masa,
This is a great article. There's not nearly enough Nextjs content out there as there should be. I guess that's because the technology is so new.
I'm pretty new to this myself, and I was wondering if you might be able to post your github repo to a google maps/nextjs project, so that I can get some context for these code snippets. Is that possible? Thanks in advance either way!
I am a bit confused by this particular snippet (referencing _document.js I'm assuming)
<html>
<body>
<div id="__next">
<div id="map"></div>
</div>
</body>
</html>
What about the
<Main />
and<NextScript />
next/document components wrapped within<body></body>
?dev-to-uploads.s3.amazonaws.com/up...
Thank you, Andrew, for liking my article (and following me!).
I wasn't articulate enough about this snippet. It's the HTML code generated by Next.js, not the JS code to be compiled by Next.js. I've revised the sentence that introduces this snippet in the article.
Hope it's now clear to you.