reactjsreact-hooksuse-effectuse-stategoogle-map-react

useState not update variable


I'm using google-map-react to display places on google map with auto-suggestion feature, following example here: https://www.freakyjolly.com/google-maps-in-react-example-application. I'm using Typescript so my code is different from the blog.

I'm having a problem right now : console.log(autoComplete); in function onPlaceChanged print undefined. Any idea why this is happening, and how can I fix it?

const AutoComplete = ({map, mapApi, addplace} : {map: any, mapApi: any, addplace: any}) => {

    const options = {
        // restrict your search to a specific type of result
        types: ['address'],
        // restrict your search to a specific country, or an array of countries
        // componentRestrictions: { country: ['gb', 'us'] },
    };

    let [searchInput, setSearchInput]= React.useState<HTMLInputElement | null>();
    
    let [autoComplete, setAutoComplete] = React.useState<any>();
    
    React.useEffect(
        () => {

            const autoCompleteInstance = new mapApi.places.Autocomplete(
                searchInput!,
                options
            );
           
            autoCompleteInstance.addListener('place_changed', onPlaceChanged);
            autoCompleteInstance.bindTo('bounds', map);

            setAutoComplete(autoCompleteInstance);
            console.log(autoCompleteInstance);


            // returned function will be called on component unmount 
            return () => {
                mapApi.event.clearInstanceListeners(searchInput!);
            }
        },
        []
    );

    const onPlaceChanged = () => {
        console.log(autoComplete);
        
        const place : google.maps.places.PlaceResult = autoComplete.getPlace();

        if (!place.geometry) return;
        if (place.geometry.viewport) {
            map.fitBounds(place.geometry.viewport);
        } else {
            map.setCenter(place.geometry.location);
            map.setZoom(17);
        }

        addplace(place);
        searchInput!.blur();
    };

    const clearSearchBox = () => {
        searchInput!.value = '';
    }

    return (
        <div className='relative items-center justify-center w-full p-5 text-center'>
            <input
                className="w-4/5 text-base"
                ref={(ref) => {
                    searchInput = ref;
                }}
                type="text"
                onFocus={clearSearchBox}
                placeholder="Enter a location"
            />
        </div>
    );
}

Solution

  • Issue

    console.log(autoComplete); in function onPlaceChanged print undefined. Any idea why this is happening, and how can I fix it?

    This is because you are closing over the initial undefined autoComplete state in the callback.

    let [autoComplete, setAutoComplete] = React.useState<any>(); // undefined
    
    ...
    
    autoCompleteInstance.addListener('place_changed', onPlaceChanged); // initial "instance"
    
    ...
    
    const onPlaceChanged = () => {
      console.log(autoComplete); // initial undefined value closed over in scope
        
      ...
    };
    

    In other words, you are accessing a stale enclosure of the autoComplete state.

    Solution

    I suggest using a React ref to hold the autoCompleteInstance instead of holding it in state. A new mapApi.places.Autocomplete instance can be constructing and stored in the ref on the initial render, the effect is still used to add the listener and bind the instance to map.

    Example:

    const autoCompleteInstanceRef = React.useRef<any>(
      new mapApi.places.Autocomplete(searchInput!, options)
    );
    
    React.useEffect(() => {
      autoCompleteInstanceRef.current.addListener('place_changed', onPlaceChanged);
      autoCompleteInstanceRef.current.bindTo('bounds', map);
    
      console.log(autoCompleteInstanceRef.current);
    
      // returned function will be called on component unmount 
      return () => {
        mapApi.event.clearInstanceListeners(searchInput!);
      };
    }, []);
    
    const onPlaceChanged = () => {
      console.log(autoCompleteInstanceRef.current);
        
      const place: google.maps.places.PlaceResult = autoCompleteInstanceRef.current.getPlace();
    
      if (!place.geometry) return;
    
      if (place.geometry.viewport) {
        map.fitBounds(place.geometry.viewport);
      } else {
        map.setCenter(place.geometry.location);
        map.setZoom(17);
      }
    
      addplace(place);
      searchInput!.blur();
    };