reactjsmapbox-gl-jsreact-map-gl

How to toggle layers in Mapbox GL JS using React


I am using react-map-gl to display a Mapbox map with several layers. I now want to let the user toggle the visibility of these layers using some kind of control panel on the map. I really like the look of the Toggle Layers example on the Mapbox website, but it's all in vanilla JS not React.

The best example I've found uses checkboxes to adjust layers, but it does so by using a default style with several built-in layers, modifying the JSON which defines that style, and then passing the modified JSON to the mapStyle property of the Map component. Presently, I'm using this property to point to a style I have created in Mapbox Studio:

<Map
    initialViewState={{
        longitude: -0.239348,
        latitude: 51.592045,
        zoom: 11.5
    }}
    style={{ width: "100%", height: 400 }}
    mapStyle="mapbox://styles/mapboxuser/xxxxxxxxx"
    styleDiffing
    mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN}
    onMouseMove={onHover}
    interactiveLayerIds={['protectedSegments']}
>
    <Source type="geojson" data={boundaries}>
        <Layer {...boundaryStyle} />
    </Source>
    <Source type="geojson" data={majorRoads}>
        <Layer {...majorRoadsStyle} />
    </Source>
    <Source type="geojson" data={protectedSegments}>
        <Layer {...protectedSegmentStyle} />
    </Source>
</Map>

Obviously, that URL isn't routeable, so I couldn't for example use fetch on it.

And anyway, this approach seems odd, since the layers I'm adjusting have been added to the map on the client side. In fact, the API docs for the Layer component say:

Once a <Layer> is mounted, the following props should not change. If you add/remove multiple JSX layers dynamically, make sure you use React's key prop to give each element a stable identity.

The sample code doesn't seem to deal with this, since the layers that it is adjusting don't have React <Layer> components.

I feel like I must be missing something obvious, since this is one of the most common things someone building a web map would want to do. In Leaflet, there's even a built-in control to let you adjust the visibility of the various layer. How does one do this in react-map-gl?


Solution

  • You can use the layout property of the <Layer> component to adjust visibility of an individual layer. The trick, though, is that you need to provide the strings visible or none to the visibility property.

    Map.js

    const [layersVisibility, setLayersVisibility] = React.useReducer(
        (state, updates) => ({ ...state, ...updates }
    ), {});
    
    return (
    <Map ...>
        <Source key="boundaries" type="geojson" data={boundaries}>
            <Layer key="boundaries"
                {...boundaryStyle}
                layout={{ visibility: layersVisibility["boundaries"] }} />
        </Source>
        <ControlPanel onChange={setLayersVisibility} />
    </Map>
    )
    

    ControlPanel.js

    function ControlPanel(props) {
        const [visibility, setVisibility] = useState({
            boundaries: true,
            protectedSegments: true,
            majorRoads: true
        });
    
        useEffect(() => {
            // Convert true/false to "visible"/"none"
            const visibilityState = Object.fromEntries(
                Object.entries(visibility).map(([k, v]) => [k, v ? "visible" : "none"])
            );
            props.onChange(visibilityState);
        }, [visibility]);
    
        const onVisibilityChange = (name, value) => {
            setVisibility({ ...visibility, [name]: value });
        };
    
        return(
            <div className={styles["control-panel"]}>
                <h3>Layers</h3>
                <label><input type="checkbox"
                    checked={visibility["boundaries"]}
                    onChange={evt => onVisibilityChange("boundaries", evt.target.checked)}
                /> Boundaries</label>
            </div>
        )
    }
    
    export default React.memo(ControlPanel);