Search code examples
reactjsleafletreact-leaflet

React-Leaflet - Updating Circle objects that are stored in state, or alternatively optimizing Circles loading speed


I'm trying to create a map with a very large amount of Circle objects. these Circles' colors will change based on a user input. A lot of colors can change at once, and I want to present the changes as fast as possible.

To save up on the time of creating the Circles each time the user changes something and the map re-renders, I thought about storing the Circle objects in an array in the state. Then when the user changes something, I would want to update the Circles' properties, but without using copy methods and the like (as it contradicts the idea of creating the Circle objects only once).

I thought about making a parallel array that stores the colors, which will be updated by the user, and to store in each Circle object's pathOptions a reference to the parallel location in this array, but am not sure how to do this.

Alternatively I would be glad to hear any other directions on how to optimize the speed.

basic version, app loads Circles from array correctly, colors are static:

import locations from "../locations.json"

function Map(props){
    const [circlesArray, setCirclesArray] = useState([])

    useEffect(() => { //initializes the circlesArray
        let tempCirclesArray = []
        locations.map(location => {
            let position = [location.coordinates[1], location.coordinates[0]]
            tempCirclesArray.push(
                <CircleMarker center={position} radius={4}
                pathOptions={
                   color: "red",
                   fillColor: "red"
                   } //here pathOptions is predetermined
                />
            )
        })
        setCirclesArray(tempCirclesArray)
    }, [])

return(
        <div>
            <div id="mapid" ref={mapRef}>  
                <button onClick={clickTest}>helno</button>
                <MapContainer center={defaultPositon} zoom={mapZoom} scrollWheelZoom={true}>
                    <TileLayer
                        attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
                        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
                    />

                    //map and display the Circle objects
                    {
                        circlesArray.map(circle => {
                            return(circle)
                        })
                    }

                </MapContainer>
            </div>
        </div>
    )

Solution

  • Keeping these CircleMarker components in state is not ideal at all. From the react-leaflet docs:

    By default these props should be treated as immutable, only the props explicitely documented as mutable in this page will affect the Leaflet element when changed.

    Changing the props on these won't rerender them.

    This can be done much better by using refs. I just answered a question on this here: How to open a specific popup on map load? . You can see detailed explanation of how to combine refs and state to reach to the underlying leaflet elements. In your case, its best not to keep all these <CircleMaker> components in a state variable. Use a locations.map directly in your component, and within that map, save all refs to an object:

    import locations from "../locations.json"
    
    const MyMapComponent = {
    
      const circleRefs = React.useRef()
    
      return (
        <MapContainer {...MapContainerProps}>
          <TileLayer />
          {locations.map(location => {
            let position = [location.coordinates[1], location.coordinates[0]]
            return (
              <CircleMarker 
                preferCanvas
                center={position} 
                radius={4}
                pathOptions={...options_you_want} 
                ref={(ref) => {
                  circleRefs.current[location.id] = ref;
                }}
              />
            )
          })}
        </MapContainer>
      )
    }
    

    Now you have an object, circleRefs, which contains an object. The keys of that object are the unique id values of each location, and the value is the underlying leaflet L.circleMarker instance. With that, you can call leaflet's setStyle method on any of these refs. For example, circleRefs['some-unique-id'].setStyle({ fillColor: 'red' }) will set the style of that ref.

    In this way, you can change the colors of these circles without forcing rerenders. This can be done on lots of circles at once without a huge performance hit - rather than unmount (and remove) the circle component, leaflet's internals recolors the svg.

    I also added the preferCanvas prop, which for anything that extends L.path (including L.circleMarker), opts to use a canvas instead of svgs. For large numbers of items, this can help performance as well.