mapbox / mapbox-gl-geocoder

Geocoder control for mapbox-gl-js using Mapbox Geocoding API
https://mapbox.com/mapbox-gl-js/example/mapbox-gl-geocoder/
ISC License
364 stars 180 forks source link

Geocoder input reinitializes repeatedly in dev mode (react + useEffect) #406

Open dispatchr1 opened 3 years ago

dispatchr1 commented 3 years ago

System Info

OS macOS 10.13.6 High Sierra Node version 14.15 Libraries:

Symptoms

I'm using the geocoder js library in a react application and seeing that the input is rendered each time my development server restarts (create react app fast refresh). I believe the appropriate enhancement is to provide a clean up function to remove the geocoder on component unmount, similar to the map.remove() function provided by mapboxgl.Map() as per the react example repo

Code sample

import { useEffect } from 'react'
import mapboxgl from 'mapbox-gl'
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'

const geocoder = new MapboxGeocoder({
  accessToken: process.env.REACT_APP_MB_TOKEN,
  mapboxgl: mapboxgl
})

export default function Autocomplete() {
  useEffect(() => {
    geocoder.addTo('#location')
    geocoder.on('result', function ({ result }) {
      console.dir(result)
    })
  }, [])
  return <div id='location' />
}

Screenshot

Autocomplete

Digital-Coder commented 2 years ago

@dispatchr1 did you solve it ?

wwwhatley commented 2 years ago

Also having this issue.

JeffThorslund commented 2 years ago

Just like we add children to our container with the addTo method, we can remove them using the Node.removeChild API. This would live in the cleanup callback of our useEffect hook.

  useEffect(() => {
    geocoder.addTo("#geocoder");

    return () => {
      const parentContainer = document.getElementById("geocoder");
      const geoChild = document.getElementsByClassName(
        "mapboxgl-ctrl-geocoder"
      );

      if (!parentContainer || !geoChild) return;
      parentContainer.removeChild(geoChild[0]);
    };
  }, []);

If you are weary about a change in className breaking your implementation. Then you can loosen the coupling by removing all children on every cleanup. Below I should only the cleanup callback.

return () => {
      const element = document.getElementById("geocoder");

      if (!element) return;

      while (element.firstChild) {
        element.removeChild(element.firstChild);
      }
    };
lekhnath commented 11 months ago

This is how I solved.


import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import mapboxgl from 'mapbox-gl';
import { useEffect, useRef, useState } from 'react';

mapboxgl.accessToken = import.meta.env.APP_MAPBOX_PUBLIC_TOKEN;

const geocoder = new MapboxGeocoder({
    accessToken: mapboxgl.accessToken,
    types: 'country,region,place,postcode,locality,neighborhood'
});

interface PlaceAutocompleteProps {

}

export default function PlaceAutocomplete(_props: PlaceAutocompleteProps) {
    const autocompleteRef = useRef<HTMLDivElement>(null);
    const [result, setResult] = useState<any>(null);

    useEffect(() => {
        const onResult = (e: any) => {
            setResult(e.result);
        }

        const onClear = () => {
            setResult(null);
        }

        geocoder.addTo(autocompleteRef.current);
        geocoder.on('result', onResult);
        geocoder.on('clear', onClear);

        return () => {
            geocoder.off('result', onResult);
            geocoder.off('result', onClear);

            if(autocompleteRef.current) {
                autocompleteRef.current.innerHTML = ''; // <-------------- This line does the trick
            }
        }
    }, []);

    return (<>
        <div className='w-full' ref={autocompleteRef}></div>
        <pre>{JSON.stringify(result, null, 2)}</pre>
    </>)
}