Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement Google Maps JS API in React without an external library?

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>
like image 748
MoSheikh Avatar asked Aug 01 '17 05:08

MoSheikh


People also ask

Can I use Google Maps API without API key?

API keys are required for apps and projects that use the Google Maps Platform APIs and SDKs.


2 Answers

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

like image 101
Rodrigo Avatar answered Oct 12 '22 19:10

Rodrigo


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

};
like image 25
Rich Avatar answered Oct 12 '22 17:10

Rich