I'm trying to wrap my head around implementing external APIs in React, and want to be able to use Google Maps' API to display a map in a child component. Ideally, I want to understand how to do this without any external libraries to get a fundamental understanding of the process before using something like Axios.
My question is this: how do I use the following snippet from the Google documentation for the API within React?
<script async defer
src='https://maps.googleapis.com/maps/api/js?key=AIzaSyDZfVO29Iytspv4xz7S68doIoiztiRLhbk&callback=initMap'>
</script>
I tried using it within my index.html file but when I make a reference to the google object in a child component in React, I get an error:
./src/Main.js Line 114: 'google' is not defined no-undef
Even if it's not the preferred or most elegant way to do it, some basic understanding on how to implement the API without any external library would be greatly appreciated. Thank you!
EDIT:
My App.js:
import React, { Component } from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import Main from './Main';
import logo from './logo.svg';
import './App.css';
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();
class App extends Component {
render() {
return (
<MuiThemeProvider>
<Main />
</MuiThemeProvider>
);
}
}
export default App;
My Main.js:
import React, { Component } from 'react';
import { FlatButton, Dialog, Card, Drawer, Paper, AppBar, Popover, Menu, MenuItem } from 'material-ui';
var items = [
{
id: 0,
name: 'Test 1',
city: 'Toronto',
longitude: 24.42142422,
latitude: 49.24121415,
tags: ['vegan', 'cheap', 'low-calorie'],
reviews: [
{
rating: 5,
reviewText: 'This was an amazing restaurant. Incredibly fast service, a large variety of options, and delicious food. I\'ll be here often',
author: 'Mohammad Sheikh',
date: new Date(),
helpfulCount: 5,
notHelpfulCount: 4
},
{
rating: 2,
reviewText: 'Absolutely horrible. Please stop making food.',
author: 'Dissatisfied Customer',
date: new Date(),
helpCount: 2,
notHelpfulCount: 3
},
],
foods:
[
{
id: 0,
name: 'Salad',
img: 'http://www.images.google.com/',
tags: ['vegan', 'low-calorie', 'cheap'],
nutrition:
{
calories: 300,
fat: 5,
carbs: 40,
protein: 24
},
reviews:
{
rating: 4,
reviewText: 'Decent salad. Would recommend.',
author: 'Vegan Bro',
date: new Date(),
helpCount: 4,
notHelpfulCount: 1
}
},
{
id: 1,
name: 'Pasta',
img: 'http://www.images.google.com/',
tags: ['vegetarian', 'dinner'],
nutrition:
{
calories: 800,
fat: 40,
carbs: 80,
protein: 20
},
reviews:
{
rating: 5,
reviewText: 'Absolutely amazing',
author: 'Food Fan',
date: new Date(),
helpCount: 8,
notHelpfulCount: 4
}
},
],
},
];
const paperStyle = {
height: 100,
width: 100,
margin: 20,
textAlign: 'center',
display: 'table',
position: 'relative',
clear: 'both',
float: 'right',
zIndex: 6
};
const paperContent = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}
class RestaurantDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false
}
}
render() {
return (
<Dialog>
</Dialog>
)
}
}
class RestaurantButton extends React.Component {
constructor(props) {
super(props);
}
handleClick = () => {
}
render() {
return (
<FlatButton style={{width: '100%', height: '64px'}} onClick>
{this.props.item.city}
<RestaurantDialog restaurant={this.props.item.name} />
</FlatButton>
)
}
}
class MapComponent extends React.Component {
constructor(props) {
super(props);
this.googleChecker = this.googleChecker.bind(this);
this.renderMap = this.renderMap.bind(this);
}
googleChecker() {
if (!window.google.maps) {
setTimeout(this.googleChecker, 100);
}
else {
this.renderMap();
}
}
renderMap() {
var map = google.maps.Map(document.getElementById('map'), {
zoom: 4,
center: {lat: 0, lng: 0}
});
}
componentDidMount() {
this.googleChecker();
}
render() {
const selections = this.props.currentSelections;
const buttons = items.filter((item) => {
for (let i = 0; i < selections.length; i++) {
if (selections.map((selection) => {return selection.toLowerCase()}).indexOf(item.tags[i].toLowerCase()) > -1) {
return true;
}
}}).map((item) => {
return (
<RestaurantButton style={{zIndex: '5'}} item={item} />
)
});
return (
<Paper id='map' zDepth={3} style={{height: '300px', width: '100%', backgroundColor: 'white', position: 'absolute'}}>
{ buttons }
</Paper>
)
}
}
class SelectionIcon extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Paper circle={true} zDepth={5} style={this.props.style} key={this.props.index} onClick={this.props.close} >
<div style={paperContent}>{this.props.item}</div>
</Paper>
)
}
}
class SelectionIcons extends React.Component {
constructor(props) {
super(props);
}
handleSelectionClose = (e) => {
e.currentTarget.open = false;
}
render() {
let currentSelections = this.props.currentSelections.slice();
let list = currentSelections.map((item, i) => {
return (
<Paper circle={true} zDepth={5} style={paperStyle} key={i} onClick={this.handleSelectionClose}>
<div style={paperContent}>{item}</div>
</Paper>
)
});
return (
<div>
{list}
</div>
)
}
}
class Main extends React.Component {
constructor(props){
super(props);
this.state = {
navMenuOpen: false,
currentSelections: []
}
}
handleMenuButtonTouch = (e) => {
this.setState({
anchorEl: e.currentTarget.parentNode,
navMenuOpen: !this.state.navMenuOpen
})
}
handleRequestChange = (change) => {
this.setState({
navMenuOpen: change.open
})
console.log(document.getElementById('test').style);
}
handleMenuClick = (e) => {
let currentSelections = this.state.currentSelections.slice();
if (currentSelections.indexOf(e) > -1) {
currentSelections.splice(currentSelections.indexOf(e), 1);
}
else {
currentSelections.push(e);
}
console.log(currentSelections)
this.setState({ currentSelections });
}
render() {
return (
<div>
<AppBar title='The App' id='test' zDepth={1} onLeftIconButtonTouchTap={this.handleMenuButtonTouch} style={{zIndex: 4}}>
</AppBar>
<Drawer
id='test2'
open={this.state.navMenuOpen}
onRequestChange={() => {this.handleRequestChange;}}
containerStyle={{zIndex: 3, marginTop: '64px'}}>
<Menu>
<MenuItem primaryText='High Protein' onClick={() => this.handleMenuClick('High Protein')} />
<MenuItem primaryText='Vegetarian' onClick={() => this.handleMenuClick('Vegetarian')} />
<MenuItem primaryText='Vegan' onClick={() => this.handleMenuClick('Vegan')} />
</Menu>
</Drawer>
<MapComponent items={items} currentSelections={this.state.currentSelections} />
<SelectionIcons currentSelections={this.state.currentSelections} />
</div>
)
}
}
export default Main;
My index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<script async defer
src='https://maps.googleapis.com/maps/api/js?key=AIzaSyDZfVO29Iytspv4xz7S68doIoiztiRLhbk'>
</script>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
API keys are required for apps and projects that use the Google Maps Platform APIs and SDKs.
The issue has to do with how async and defer works when using the google maps API.
Basically the google API is not loaded yet when your code reaches the point when the map has to be rendered. Please take a look at this post to understand how that works:
https://stackoverflow.com/a/36909530/2456879
There are two solutions.
SOLUTION ONE Don't use async and defer in your script tag in order to allow the script to be downloaded before executing your app:
<script src='https://maps.googleapis.com/maps/api/js?key=AIzaSyDZfVO29Iytspv4xz7S68doIoiztiRLhbk&callback=initMap'>
</script>
SOLUTION TWO Create some sort of recursive checker in order to see if the google api has loaded in order to continue your app's execution once the google maps api is availabe:
class MyMap extends Component{
constructor(props){
super(props);
this.googleChecker = this.googleChecker.bind(this);
this.renderMap = this.renderMap.bind(this);
}
googleChecker() {
// check for maps in case you're using other google api
if(!window.google.maps) {
setTimeout(googleChecker, 100);
console.log("not there yet");
} else {
console.log("we're good to go!!");
// the google maps api is ready to use, render the map
this.renderMap();
}
}
renderMap(){
const coords = { lat: 41.375885, lng: 2.177813 };
// create map instance
new google.maps.Map(this.refs.mapContainer, {
zoom: 16,
center: {
lat: coords.lat,
lng: coords.lng
}
});
}
componentDidMount(){
this.googleChecker();
}
render(){
return(
<div className="card map-holder">
<div className="card-block" ref="mapContainer" />
</div>
);
}
}
You could also use a promise and resolve it in the checker method or something similar. Also you can put that code in a parent component, store a boolean in the state and pass that to the child components in order to start rendering the map(s) once the api is available. This approach also could be used with redux and redux thunk in order to resolve a promise. As you can see there are a few alternatives depending on your approach.
Here's a live sample using the timeout checker:
https://jsbin.com/tejutihoka/edit?js,output
...with Hooks only. This solution uses library, however it is Google's own loader: https://developers.google.com/maps/documentation/javascript/overview#js_api_loader_package
// https://developers.google.com/maps/documentation/javascript/overview#js_api_loader_package
import { useState, useEffect, useRef } from "react";
import { Loader } from "@googlemaps/js-api-loader";
export default function Map({
apiKey = "",
label = "",
zoom = 16,
coords = {
lat: 0,
lng: 0,
},
}) {
const [gmapWin, setGmapWin] = useState(false);
const [gmapObj, setGmapObj] = useState();
const mapBox = useRef();
const props = useRef({ apiKey, label, coords, zoom });
// load map
useEffect(() => {
const loader = new Loader({
apiKey: props.current.apiKey,
version: "weekly",
});
// https://stackoverflow.com/a/61980156
const abortController = new AbortController();
(async function () {
loader.load().then(() => {
if (
!abortController.signal.aborted &&
window.google?.maps &&
!gmapWin
) {
setGmapWin(true);
}
if (gmapWin) {
setGmapObj(
new window.google.maps.Map(mapBox.current, {
center: props.current.coords,
zoom: props.current.zoom,
})
);
}
});
})();
return () => {
abortController.abort();
};
}, [gmapWin]);
// add marker
useEffect(() => {
if (gmapObj) {
new window.google.maps.Marker({
position: props.current.coords,
map: gmapObj,
label: props.current.label,
});
}
}, [gmapObj]);
return <div className="map" ref={mapBox} />;
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With