React/Material UI - Google Places autocomplete dropdown sometimes doesn't work

1.5k Views Asked by At

So this code is working, but sometimes on page load or refresh the google places dropdown autocomplete suggestions is not showing up, I can type anything in the searchbox but it just doesn't load the autocomplete box. What could cause this issue?

This is the code.

const apiKey = process.env.REACT_APP_GOOGLE_KEY;
const mapApiJs = 'https://maps.googleapis.com/maps/api/js';
const geocodeJson = 'https://maps.googleapis.com/maps/api/geocode/json';


// load google map api js

function loadAsyncScript(src) {
  return new Promise(resolve => {
    const script = document.createElement("script");
    Object.assign(script, {
      type: "text/javascript",
      async: true,
      src
    })
    script.addEventListener("load", () => resolve(script));
    document.head.appendChild(script);
  })
}

const extractAddress = (place) => {

  const address = {
    city: "",
    state: "",
    zip: "",
    country: "",
    plain() {
        const city = this.city ? this.city + ", " : "";
        const zip = this.zip ? this.zip + ", " : "";
        const state = this.state ? this.state + ", " : "";
        return city + zip + state + this.country;
    }
  }

  if (!Array.isArray(place?.address_components)) {
    return address;
  }

  if(typeof place.geometry.location.lat === 'function') {
    address.lat = place.geometry.location.lat();
  } else {
    address.lat = place.geometry.location.lat;
  }

  if(typeof place.geometry.location.lng === 'function') {
    address.lng = place.geometry.location.lng();
  } else {
    address.lng = place.geometry.location.lng;
  }

  place.address_components.forEach(component => {
    const types = component.types;
    const value = component.long_name;

    if (types.includes("locality")) {
      address.city = value;
    }

    if (types.includes("administrative_area_level_2")) {
      address.state = value;
    }

    if (types.includes("postal_code")) {
      address.zip = value;
    }

    if (types.includes("country")) {
      address.country = value;
    }

  });

  return address;
}


function ReportForm() {

    const searchInput = useRef(null);
    const [address, setAddress] = useState({});

    const [coords, setCoords] = useState({
        lat: 42.680115925419294,
        lng: 14.010667884881462,
    });

    // init gmap script
    const initMapScript = () => {
        // if script already loaded
        if(window.google) {
          return Promise.resolve();
        }
        const src = `${mapApiJs}?key=${apiKey}&libraries=places&v=weekly`;
        return loadAsyncScript(src);
    }

    // do something on address change
    const onChangeAddress = (autocomplete) => {
        const place = autocomplete.getPlace();
        setAddress(extractAddress(place));
        setCoords({
            lat: place.geometry.location.lat(),
            lng: place.geometry.location.lng(),
        });
    }

    // init autocomplete
    const initAutocomplete = () => {
        if (!searchInput.current) return;

        const autocomplete = new window.google.maps.places.Autocomplete(searchInput.current);
        autocomplete.setFields(["address_component", "geometry"]);
        autocomplete.addListener("place_changed", () => onChangeAddress(autocomplete));

    }

    const reverseGeocode = ({ latitude: lat, longitude: lng}) => {
        const url = `${geocodeJson}?key=${apiKey}&latlng=${lat},${lng}`;
        searchInput.current.value = "Getting your location...";
        fetch(url)
            .then(response => response.json())
            .then(location => {
              const place = location.results[0];
              const _address = extractAddress(place);
              setAddress(_address);
              searchInput.current.value = _address.plain();
              setCoords({
                lat: lat,
                lng: lng,
              });
            })
    }

    const findMyLocation = () => {
        if (navigator.geolocation) {
          navigator.geolocation.getCurrentPosition(position => {
            reverseGeocode(position.coords)
          })
        }
    }

    // load map script after mounted
    useEffect(() => {
        initMapScript().then(() => initAutocomplete())
    }, []); 

    return(
        <>        
            <form>                    
                <div>
                    <div className="search">
                        <span><Search /></span>
                        <input 
                            name="address" 
                            ref={searchInput} 
                            type="text" 
                            placeholder="Search Address...." 
                        />
                        <button type="button" onClick={findMyLocation}><GpsFixed /></button>
                    </div>
                </div>                      
            </form>                       
        </>
    );
}

I have also tried with Material UI TextField and inputRef instead of the standard input, but I get the same problem. Also, it happens more often on page refresh. On first page load it almost works every time.

1

There are 1 best solutions below

0
On BEST ANSWER

Have you tried adding searchInput.current to the dependency array of useEffect? You have this line if (!searchInput.current) return; inside initAutocomplete and your searchInput is initially set to null. So on page refresh it's possible there is a race condition between the ref being set on the input element and the running of the initAutocomplete function.