mapbox-gl-geocoder
mapbox-gl-geocoder copied to clipboard
Geocoder input reinitializes repeatedly in dev mode (react + useEffect)
System Info
OS macOS 10.13.6 High Sierra Node version 14.15 Libraries:
- mapbox-gl
- MapboxGeocoder
- create-react-app
- tailwindcss@2
- craco
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
@dispatchr1 did you solve it ?
Also having this issue.
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);
}
};
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>
</>)
}