React Mapbox component flickering when I type in the input box of a different component Code Answer

I want to have a map and an input form on a single page. The user should be able to type into the input field, hit submit, and have the Mapbox component move to that location.

I have two issues, and I suspect they are related. When I type in the input box, the map component flickers after each onChange event. Second, when I submit the form, the geolocation is successfully fetched, the props are updated on the Map component and the map marker appears, but the map doesn’t perform the neat animation I see when I just go into React Dev tools and manually update the props.

I know I’m a rank beginner, and there are a thousand things wrong with this I’m sure. I’d love if someone knows the answer to this, but I’d also be happy if someone could just point me in the right direction. I spend the morning looking over the React’s “getting started” and “hooks” section, and took some React classes where you built simple apps, but clearly there was something fundamental that didn’t sink in. Thanks!

Here is the NextJS page:

import ReactMapboxGl, { Marker } from 'react-mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { useState } from 'react';

export default function TestPage() {
  const Map = ReactMapboxGl({
    accessToken: process.env.NEXT_PUBLIC_MAPBOX,
  });

  const [searchLocation, setSearchLocation] = useState('');
  const [geolocate, setGeolocate] = useState({
    coordinates: [2.61878695312962, 47.8249046208979],
    isPin: false,
  });

  async function fetchGeolocate(location) {
    const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${location}.json?access_token=${process.env.NEXT_PUBLIC_MAPBOX}`;
    fetch(url)
      .then((response) => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.text();
      })
      .then((myText) => {
        const json = JSON.parse(myText);
        const coordinates = json.features[0].center;
        setGeolocate({ coordinates, isPin: true });
      });
  }

  function handleInputChange(event) {
    setSearchLocation(event.target.value);
  }
  function handleForm(e) {
    e.preventDefault();
    fetchGeolocate(searchLocation);
  }

  return (
    <>
      <Map
        style="mapbox://styles/mapbox/streets-v11"
        containerStyle={{
          height: '50vh',
          width: '100vw',
        }}
        center={geolocate.coordinates}
        zoom={[3]}
      >
        {geolocate.isPin && (
          <Marker coordinates={geolocate.coordinates}>
            <span className="text-3xl" role="img" aria-label="push-pin">
              📌
            </span>
          </Marker>
        )}
      </Map>

      <form onSubmit={handleForm}>
        <div className="flex flex-col mb-4">
          <label
            htmlFor="search"
            className="flex flex-col uppercase font-bold text-lg text-gray-700"
          >
            Location
            <input
              type="text"
              name="search"
              placeholder="Location"
              value={searchLocation}
              className="border py-2 px-3 text-gray-700 block"
              onChange={handleInputChange}
            />
          </label>
        </div>
        <button type="submit">Find</button>
      </form>
    </>
  );
}

Answer

Your sense that the problems are related is right on. One issue is that you’re re-initializing the Map component on every render:

const Map = ReactMapboxGl({
    accessToken: process.env.NEXT_PUBLIC_MAPBOX,
  });

It’s also going to keep animations from happening (I think) because it’ll make a new instance of the Map, which will cause React unmount/remount the component.

Edit: Answer from OP below: initialize outside of component

Looks like you can just initialize the component outside any rendering logic.

Alternate approach: using state

In functional components, we want to put initialization logic in useEffect (or if it’s crucial that it happens before first render, useLayoutEffect).

const [map, setMap] = useState()
useEffect(() => {
  const mapInit = ReactMapboxGl({
    accessToken: process.env.NEXT_PUBLIC_MAPBOX,
  })
  setMap(mapInit)
}, [])

The [] at the end is key. It keeps our callback from getting called after the first time.

To be honest, your component may be complex enough to warrant using a class component, where you could use componentDidMount for the initialization, and all those helper functions won’t have to be recomputed each render. Ultimately it’s up to you though.

One last note: you’ve declared fetchGeolocate as async, which means instead of calling .then on all the results, you can use await if you want. Or take away the async & continue to use Promise.then, it’s up to you. Mozilla’s docs might help clarify the difference.

Related Posts

© No Copyrights, All Questions are retrived from public domain.
Tutorial Guruji