17

I want to have an auto completing location search bar in my react component, but don't know how I would go about implementing it. The documentation says to include

<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initMap" async defer></script>

in an HTML file, and then have an initialize function pointing to an element - how would I go about doing this with my react component/JSX? I presume I would have to import the api link, but I have no clue where to go from there.

import React from 'react';
import "https://maps.googleapis.com/maps/api/js?key=MYKEY&libraries=places&callback=initMap";

const SearchBar = () => (   
    <input type="text" id="search"/> //where I want the google autocomplete to be
);

export default SearchBar;
pretzelhammer
  • 13,874
  • 15
  • 47
  • 98
Adam.V
  • 779
  • 2
  • 9
  • 25

3 Answers3

28

Google Maps API loading via static import:

import "https://maps.googleapis.com/maps/api/js?key=MYKEY&libraries=places&callback=initMap";

is not supported, you need to consider a different options for that purpose:

  • reference Google Maps API JS library via /public/index.html file: <script src="https://maps.googleapis.com/maps/api/js?key=MYKEY&libraries=places"></script>
  • or dynamically load JS resource, for example using this library

Now regarding SearchBar component, the below example demonstrates how to implement a simple version of Place Autocomplete (without a dependency to Google Map instance) based on this official example

import React from "react";
/* global google */


class SearchBar extends React.Component {
  constructor(props) {
    super(props);
    this.autocompleteInput = React.createRef();
    this.autocomplete = null;
    this.handlePlaceChanged = this.handlePlaceChanged.bind(this);
  }

  componentDidMount() {
    this.autocomplete = new google.maps.places.Autocomplete(this.autocompleteInput.current,
        {"types": ["geocode"]});

    this.autocomplete.addListener('place_changed', this.handlePlaceChanged);
  }

  handlePlaceChanged(){
    const place = this.autocomplete.getPlace();
    this.props.onPlaceLoaded(place);
  }



  render() {
    return (
        <input ref={this.autocompleteInput}  id="autocomplete" placeholder="Enter your address"
         type="text"></input>
    );
  }
}
Luke
  • 537
  • 4
  • 10
Vadim Gremyachev
  • 57,952
  • 20
  • 129
  • 193
  • 2
    When I do this, I get the error `google' is not defined no-undef`. I put the google api script (with my own key) in my public/index.html file as per the demo, but it still doesn't recognize google. Somehow it isn't global? – Adam.V Oct 22 '18 at 20:06
  • 5
    @Adam.V, you probably forgot to put `/* global google */` on top of the file. Or could utilize [this solution](https://stackoverflow.com/a/43718073/1375553). It also contains the explanation why this error occurs – Vadim Gremyachev Oct 22 '18 at 20:21
  • 3
    You were spot on! I saw the grey text and thought it was just a comment. I didn't know it served any purpose in the js. – Adam.V Oct 23 '18 at 20:44
3

Here's a solution using ES6 + React Hooks:

First, create a useGoogleMapsApi hook to load the external script:

import { useEffect, useState, useCallback } from 'react'
import loadScript from 'load-script'
import each from 'lodash/each'

var googleMapsApi
var loading = false
var callbacks = []

const useGoogleMapsApi = () => {
  const [, setApi] = useState()

  const callback = useCallback(() => {
    setApi(window.google.maps)
  }, [])

  useEffect(() => {
    if (loading) {
      callbacks.push(callback)
    } else {
      if (!googleMapsApi) {
        loading = true
        loadScript(
          `https://maps.googleapis.com/maps/api/js?key=${process.env.REACT_APP_GOOGLE_MAPS_API_KEY}&libraries=places`,
          { async: true },
          () => {
            loading = false
            googleMapsApi = window.google.maps
            setApi(window.google.maps)
            each(callbacks, init => init())
            callbacks = []
          })
      }
    }
  }, [])

  return googleMapsApi
}

export default useGoogleMapsApi

Then, here's your input component:

import React, { useRef, useEffect, forwardRef } from 'react'
import useGoogleMapsApi from './useGoogleMapsApi'

const LocationInput = forwardRef((props, ref) => {
  const inputRef = useRef()
  const autocompleteRef = useRef()
  const googleMapsApi = useGoogleMapsApi()

  useEffect(() => {
    if (googleMapsApi) {
      autocompleteRef.current = new googleMapsApi.places.Autocomplete(inputRef.current, { types: ['(cities)'] })
      autocompleteRef.current.addListener('place_changed', () => {
        const place = autocompleteRef.current.getPlace()
        // Do something with the resolved place here (ie store in redux state)
      })
    }
  }, [googleMapsApi])

  const handleSubmit = (e) => {
    e.preventDefault()
    return false
  }

  return (
    <form autoComplete='off' onSubmit={handleSubmit}>
      <label htmlFor='location'>Google Maps Location Lookup</label>
      <input
        name='location'
        aria-label='Search locations'
        ref={inputRef}
        placeholder='placeholder'
        autoComplete='off'
      />
    </form>
  )
}

export default LocationInput

Viola!

Allan of Sydney
  • 1,410
  • 14
  • 23
1

Was making a custom address autocomplete for a sign up form and ran into some issues,

// index.html imports the google script via script tag  ie: <script src="https://maps.googleapis.com/maps/api/js?key=MYKEY&libraries=places"></script>


import {useState, useRef, useEffect } from 'React'

function AutoCompleteInput(){

const [predictions, setPredictions] = useState([]);
const [input, setInput] = useState('');
const [selectedPlaceDetail, addSelectedPlaceDetail] = useState({})
const predictionsRef = useRef();


useEffect(
()=>{
      try {
        autocompleteService.current.getPlacePredictions({ input }, predictions => {
          setPredictions(predictions);
        });
      } catch (err) {
       // do something
      }
    }
}, [input])

const handleAutoCompletePlaceSelected = placeId=>{
 if (window.google) {
      const PlacesService = new window.google.maps.places.PlacesService(predictionsRef.current);
      try {
        PlacesService.getDetails(
          {
            placeId,
            fields: ['address_components'],
          },
         place => addSelectedPlaceDetail(place)
        );
      } catch (e) {
        console.error(e);
      }
    }
}

return (
  <>
   <input onChange={(e)=>setInput(e.currentTarget.value)}
    <div ref={predictionsRef}
     { predictions.map(prediction => <div onClick={ ()=>handleAutoCompletePlaceSelected(suggestion.place_id)}> prediction.description </div> )
   }
   </div>
  <>
 )
}

So basically, you setup the autocomplete call, and get back the predictions results in your local state.

from there, map and show the results with a click handler that will do the follow up request to the places services with access to the getDetails method for the full address object or whatever fields you want.

you then save that response to your local state and off you go.

Denis S Dujota
  • 543
  • 5
  • 13