Search code examples
reactjsreact-leaflet

Use leaflet map object outside useEffect in react


I have a button that will edit the mymap object (for example add marker or circle). But it give me error that says "mymap is not defined". I have tried declare the mymap var outside the useEffect but it doesn't work because useEffect doesn't save the assignment. I also tried useState but it doesn't work either.

here's my code

import 'leaflet/dist/leaflet.css'
import L from 'leaflet/dist/leaflet'
import { useEffect, useState} from 'react'

import './css/grid.css'

export const Grid = ({mapID}) => {
    const centerCoor = //redacted
    var layerGroup = L.layerGroup()

    function addGrid(){
        var btnel = document.getElementById("btn-grid");
        btnel.classList.toggle("btn-active");

        if (btnel.classList.contains("btn-active")){
            // do something with "mymap"
            mymap.on('click', (e)=>{
            })
        }
    }

    useEffect(()=>{
        var mymap = L.map(mapID, {
            center: centerCoor,
            zoom:18
        });

        L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
            attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
            maxZoom: 18,
            id: 'mapbox/satellite-v9',
            tileSize: 512,
            zoomOffset: -1,
            accessToken: //redacted
            }
        ).addTo(mymap);


        

        return () => {
            mymap.off()
            mymap.remove()
        }
    },[])


    return (
        <div>
            <button onClick={addGrid} className="" id="btn-grid">ADD GRID</button>
            <div id={mapID}>
            </div>
        </div>
    )
}


Solution

  • Leaflet keeps track of its state separately from React, which is why it's best to use the React-Leaflet binding. If you are going to use vanilla Leaflet inside of React, you should consider setting up a useEffect hook that handles creating the map instance and setting the map instance to state. Then you can access the map instance to add event listeners, add markers/popups, etc. This also helps React keep track of Leaflet's current state and monitor for any changes. Here is an example component:

    import React, { useEffect, useRef, useState } from 'react';
    import L from 'leaflet';
    
    const MapComponent = (props) => {
      // Map state:
      const [mapInstance, setMapInstance] = useState(null);
      const [marker, setMarker] = useState(null);
    
      // Map refs:
      const mapRef = useRef(null);
      const tileRef = useRef(null);
      const markerRef = useRef(null);
    
      // Base tile for the map:
      tileRef.current = L.tileLayer(
        `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`,
        {
          attribution:
            '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
        }
      );
    
      const mapStyles = {
        overflow: 'hidden',
        width: '100%',
        height: '100vh',
      };
    
      // Options for our map instance:
      const mapParams = {
        center: [37.0902, -95.7129], // USA
        zoom: 3,
        zoomControl: false,
        zoomSnap: 0.75,
        layers: [tileRef.current], // Start with just the base layer
      };
    
      // Map creation:
      useEffect(() => {
        mapRef.current = L.map('map', mapParams);
        // Add an event listener:
        mapRef.current.on('click', () => {
          alert('map clicked');
        });
        // Set map instance to state:
        setMapInstance(mapRef.current);
      }, []); // <- Empty dependency array, so it only runs once on the first render.
    
      // If you want to use the mapInstance in a useEffect hook,
      // you first have to make sure the map exists. Then, you can add your logic.
      useEffect(() => {
        // Check for the map instance before adding something (ie: another event listener).
        // If no map, return:
        if (!mapInstance) return;
        if (mapInstance) {
          mapInstance.on('zoomstart', () => {
            console.log('Zooming!!!');
          });
        }
      }, [mapInstance]);
    
      // Toggle marker on button click:
      const handleClick = () => {
        if (marker) {
          marker.removeFrom(mapInstance);
          markerRef.current = null;
        } else {
          markerRef.current = L.marker([40.7128, -74.006]).addTo(mapInstance);
        }
        setMarker(markerRef.current);
      };
    
      return (
        <>
          <button onClick={handleClick}>
            {`Click to ${marker ? 'remove' : 'add'} marker`}
          </button>
          <div id="map" style={mapStyles} />
        </>
      );
    };
    
    export default MapComponent;
    

    Here is a live sandbox I've set up so you can test it out: LIVE DEMO