visgl / react-map-gl

React friendly API wrapper around MapboxGL JS
http://visgl.github.io/react-map-gl/
Other
7.82k stars 1.35k forks source link

[Feat] Add support for latest Mapbox rendering release (V3) #2377

Open trumbitta opened 6 months ago

trumbitta commented 6 months ago

Target Use Case

The simplest basic case with Mapbox v3 works, but as soon as you add a Source and Layer we get the Style is not done loading error.

Here's a repro: https://codesandbox.io/p/sandbox/frosty-minsky-pdqrlk?file=%2Fsrc%2FApp.js

zsloan112 commented 6 months ago

Glad I checked here today. I was getting this same error and had been fighting it for hours. Same use case and reproduction steps for me.

iostat commented 3 months ago

Been hitting this as well.

As a workaround, you can wrap everything inside your <Map...> with a component that listens for the style.load and only populates children of the <Map> once that event has been fired.

It can get a little hairy if you start doing stuff like changing the map style dynamically (totally doable, but hairy!), but something like the following can get you started.

import {useEffect, useState} from 'react';
import {default as ReactMapGL, useMap} from 'react-map-gl';

type StyleLoadedGuardProps = {
  // this state has to come from outside the StyleLoadedGuard component as otherwise
  // it'll get cleared if/when the map changes. And the guard has to be its own component
  // as it seems like that's the only way to get a ref to the map via useMap()...
  guardState: [boolean, React.Dispatch<React.SetStateAction<boolean>>]; // useState(false)
  children?: React.ReactNode;
};
const StyleLoadedGuard: React.FC<StyleLoadedGuardProps> = props => {
  const mapRef = useMap();
  const [styleLoadedAtLeastOnce, setStyleLoadedAtLeastOnce] = props.guardState;
  useEffect(() => {
    if (mapRef.current) {
      const map = mapRef.current;

      const onStyleLoad = () => {
        setStyleLoadedAtLeastOnce(true);
      };
      map.on('style.load', onStyleLoad);
      if (map.isStyleLoaded()) {
        onStyleLoad();
      }
      return () => {
        map.off('style.load', onStyleLoad);
      };
    } else {
      return undefined;
    }
  }, [mapRef]);

  return styleLoadedAtLeastOnce && props.children;
}

export const MyMap: React.FC<ReactMapGL.MapProps> = (props) => {
  const styleLoadedGuardState = useState(false);
  const mapProps = {...props, children: undefined}; // to be safe
  return <ReactMapGL.Map {...mapProps}>
    <StyleLoadedGuard guardState={styleLoadedGuardState}>
      {props.children}
    </StyleLoadedGuard>
  </ReactMapGL.Map>
}

/// should just work!
export const UseAMap: React.FC = () =>
  <MyMap>
    <ReactMapGL.Source ...>
      <ReactMapGL.Layer ... />
      <ReactMapGL.Layer ... />
      <ReactMapGL.Layer ... />
    </ReactMapGL.Source>
  </MyMap>;

I actually wanted to take a stab at making a PR that implements similar behavior on Source and Layer (there already is some similar logic for previous versions of mapbox-gl-js), but I'm not entirely sure what's the best way for me to get a dev environment going with fast-refresh and stuff.

sepehr500 commented 1 month ago

Is there going to be a fix for this? The above won't work if you are trying to switch layers

backuardo commented 2 weeks ago

@sepehr500 - the above solution wasn't working for my app (updating mapStyle based on global state), but this seems to work (even though it's nasty).

const SafeChildren = ({ children }) => {
  const { current: map } = useMap();
  const mapStyle = useStore((state) => state.mapStyle); // My map style in Zustand
  const [canRenderChildren, setCanRenderChildren] = useState(false);

  useEffect(() => {
    if (!map || canRenderChildren) return;

    const checkStyleLoaded = () => {
      if (map.isStyleLoaded() && map.getStyle()) {
        setCanRenderChildren(true);
      }
    };

    checkStyleLoaded();
    const interval = setInterval(checkStyleLoaded, 10); // 😖

    return () => clearInterval(interval);
  }, [map, mapStyle, canRenderChildren]);

  return canRenderChildren ? <>{children}</> : null;
};

Usage:

<MapProvider>
  <Map>
    <SafeChildren>
      <Source>
        <Layer />
      </Source>
    </SafeChildren>
  </Map>
</MapProvider>
gucr commented 1 week ago

Here is how we approached it to load layers on the fly

function RenderAfterMap({children}) {

  const map = useMap()
  const [canRender, setCanRender] = useState(false)

  useEffect(() => {
    map.current?.on('load', () => setCanRender(true))
  }, [map])

  return <>{canRender && children}</>
}