8

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>
MoSheikh
  • 769
  • 1
  • 11
  • 20

2 Answers2

8

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

Rodrigo
  • 1,638
  • 1
  • 15
  • 27
  • 1
    Thanks for the help, but even after implementing the googleChecker method into my component, I get the same error saying that "'google' is not defined". This is trying a combination of including and removing the async & defer tags in the html, as well as taking out the googleChecker, componentDidMount and renderMap methods. – MoSheikh Aug 02 '17 at 00:18
  • @MoSheikh can you share a simple live sample of you code in jsbin?, just a barebone sample that shows how you are getting google maps and using it on React. That would help to solve this faster. – Rodrigo Aug 02 '17 at 02:46
  • Here's a sample without the helper functions: https://jsbin.com/ginimayifa/1/edit?html,css,js,output and here's another with the helper functions you specified: https://jsbin.com/nikeyovije/edit?html,css,js,output. The latter actually works within jsbin but doesn't in my local environment for some reason. I've copied it exactly to my environment but it still doesn't work for some reason. – MoSheikh Aug 02 '17 at 03:41
  • I've added my files to the main question in an edit, help would be greatly appreciated! – MoSheikh Aug 02 '17 at 03:50
  • Can you create a live sample with your code that is reduced to a bare minimum, like this?: https://jsbin.com/masowev/edit?html,js I've never used material ui and perhaps that could be creating the issue. Have you tried getting rid of everything but the main and map components, just drop the rest. Normally a good way to track bugs is start with the simplest version of the app and start adding things until the issue comes up. For what I see that code should be working, but is a lot to go through. Finally avoid using `document.getElementById()` in React, if you must use refs. – Rodrigo Aug 02 '17 at 04:51
  • Hi, I am a total newbie in React, How did you import google and window in this script ? I can't figure it out on jsbin example too. Thanks for help ! – aa bb Mar 24 '21 at 10:40
  • @aabb actually there is no importing here. The google maps script is being added as a ` – Rodrigo Mar 24 '21 at 13:43
  • @Rodrigo I understand but if the js file is not yet loaded, how to make `google` not raising an error at compilation ? – aa bb Mar 24 '21 at 16:16
  • @aabb That's the reason behind the `googleChecker` method, it checks every 100 milliseconds if the script has been loaded, and when it is it runs the `renderMap` method. – Rodrigo Mar 24 '21 at 23:15
  • @Rodrigo. I got this error : "Line 29:13: 'google' is not defined no-undef This error occurred during the build time and cannot be dismissed." Are you sure that this is the error as it bugs during build time ? I am a beginner also with nodejs. Is there an option on the node js server to compile even if some objects are not defined ? Could it come from that ? – aa bb Mar 25 '21 at 16:23
  • If you're running into that error perhaps ESLint is raising that warning, try adding `// eslint-disable-next-line` above this line `new google.maps.Map(this.refs.mapContainer, {` or try using this at the top of your component file: `/* global google */`. If you keep running into issues I'd recommend you to use this package: https://www.npmjs.com/package/react-google-maps – Rodrigo Mar 25 '21 at 20:18
2

...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} />;

};
Rich
  • 705
  • 9
  • 16