Unnecessary re-renders on react-leaflet choropleth map

Viewed 230

First of all, I'm new to React and the whole Frontend thing, so this is probably something obvious.

I'm writing a simple choropleth map in React using react-leaflet, but I'm facing some performance issues.

My case is very similar to the one you can see here:

https://codepen.io/hazmi/pen/JjoyONo

If you add a console.log statement to the style method and check the console you can see that the style method is called for every feature on the map on every mouseover or mouseout event.

In my case it is even worse, as style is called twice for every feature on the map.

Unfortunately, this is a huge performance issue on bigger maps and also causes all kinds of side effects.

I assume the style method is called because the <GeoJSON> layer is re-rendered all the time because of the state change caused by the 2 mouse event handlers setting the selected variable. My case is very similar, just the component structure is a bit more complex.

More components rely on the value of the selected variable, so I need that to be set on hover and reset on mouseout.

const colors = [ /* constant array */ ];

function Map() {

    const [hoveredCountry, setHoveredCountry] = useState(null);
    const [statsData, setStatsData] = useState(null);

...

    const highlightFeature = e => {
        let layer = e.target;
        setHoveredCountry({ stats: statsData[layer.feature.properties.iso_a3] });
    }

    const resetHighlight = e => {
        setHoveredCountry(null);
    }

    const onEachFeature = (feature, layer) => {
        layer.on({
            mouseover: highlightFeature,
            mouseout: resetHighlight
        });
    }

    const style = feature => { /* calculate and return style object */ }

...

    return (
        <MapContainer center={[40, 0]}>
            <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" attribution='&copy; <a href="https://www.openstreetmap.org/copyright" target="blank">OpenStreetMap</a> contributors' />
            <ZoomControl position='bottomleft' />
            <GeoJSON data={countries} style={style} onEachFeature={onEachFeature} ref={geoJsonLayer}>
                <InfoTooltip scope={dataScope} hoveredCountry={hoveredCountry} />
            </GeoJSON>
            <InfoBox data={selectedCountry} scope={dataScope} open={infoBoxOpen} closeHandler={handleInfoBoxClosed} />
            <MapControls options={dataScopes} value={dataScope} changeHandler={handleDataScopeChange} />
           <Legend scope={dataScope} colors={colors} hoveredCountry={hoveredCountry} />
        </MapContainer>
    );
}

In my case shown above, the Legend and InfoTooltip both depend on the hoveredCountry state variable similarly how the tooltip uses the selected state variable in the CodePen example.

How can I avoid these unnecessary re-renders? Is this the issue to begin with?

1 Answers

I assume the style method is called because the <GeoJSON> layer is re-rendered all the time because of the state change caused by the 2 mouse event handlers setting the selected variable.

You are on the right track indeed.

What happens is that React thinks it has to rerender that component because one of its props has changed:

<GeoJSON data={countries} style={style} onEachFeature={onEachFeature}>

Your countries variable (actually data in your CodePen) and style function are defined outside of your App component, so they are fine.

On the other hand, your onEachFeature function is declared in the body of your component. So everytime React reevaluates the component, and in particular whenever one of its state is changed, it reruns the App function, which creates a new onEachFeature function (which does exactly the same thing, but is is a different reference). Hence the <GeoJSON> component is rerendered.

And you had to keep it in there because it uses your state setter function.

A solution is simply to use the useCallback hook, which is made for such purpose: avoid a reference change whenever the component is reevaluated:

This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders

In your case:

const onEachFeature = useCallback((feature, layer) => {
    layer.on({
        mouseover: highlightFeature,
        mouseout: resetHighlight
    });
}, []);

Note: inlining highlightFeature and resetHighlight would also make it clearer that you need them only when building the callback.

Related