1

I am using react + redux and react-leaflet v3 + an API to load markers to pin on a map dynamically based on panning behavior.

I have it so that "dragend" event is triggered and the map boundaries, zoom level and center are calculated in order to create the appropriate parameters to feed into the API endpoint. This way, as the user pans the map, the markers are fed in dynamically.

The marker data is saved in the redux store and the API call is triggered via useEffect listening on endpoint changes.

The problem is that it seems that all the markers are re-rendered at every pan and this makes the application jittery and slow. I would like it so that only new markers are rendered and the old markers are not re-rendered. Additionally, old markers that are outside the boundary should simply be removed. However, this is just not the case. I thought that as long as the markers have a key, that react-redux would be able to compare the old data with the new and only render the new components.

My marker loading is done via createAyncThunk like so:

export const getCities = createAsyncThunk("markers/getCities", async (endpoint, thunkAPI) => {
    try {
        const response = await axios.get(endpoint);
        return response.data;
    } catch (error) {
         return thunkAPI.rejectWithValue({ error: error.message });
    }
});

with the following slice:

// CREATE SLICE
const markerSlice = createSlice({
  name: "markers",
  initialState: {
    cities: [],
    markerType: "cities",
    endpoint: "/api/get/cities",
  },
  reducers: {
  // some reducer code
  },
  extraReducers: (builder) => {
    builder.addCase(getCities.pending, (state) => {
        state.cities = [];
    });
    builder.addCase(getCities.fulfilled, (state, { payload }) => {
        state.cities = payload;
    });
    builder.addCase(getCities.rejected,(state, action) => {
        state.loading = "error";
    });
  }
});

and my Map component is like so (simplified for readability):

import React, { useEffect } from "react";
import { MapContainer, Marker, Popup, TileLayer, useMap, useMapEvents } from "react-leaflet";
import "../../css/app.css";
import { useSelector, useDispatch, batch } from "react-redux";
import { getCities, setEndpoint } from "../features/markerSlice";
import { setLatBnd, setLngBnd, setZoom, setLat, setLng } from "../features/mapSlice";

export const LeafMap = () => {
    const dispatch = useDispatch();

    //marker state (marker data)
    const stateMarker = useSelector(state => state.marker);

    // map state (center, zoom level, bounds, etc)
    const stateMap = useSelector(state => state.map);

    // This calls the API to retrieve data
    useEffect(() => {
        dispatch(getCities(stateMarker.endpoint));
    }, [stateMarker.endpoint]);

    // Custom Marker Component to render markers
    const GetMarkers = () => {
        const markerType = stateMarker.markerType;

            return stateMarker.cities.map((el, i) => (
              <Marker
                key={i}
                position={[el.latitude, el.longitude]}
              >
              </Marker>
            ));
    };

    // This is a child component to MapContainer (leaflet v3) which listens to map events
    function GetMapProperties() {
        const map = useMap();
        // listen to drag event end
        useMapEvents({
            dragend: () => {
                // Get map info in order to create endpoint parameters for API call
                const bounds = map.getBounds();
                const latBnd = bounds['_northEast'].lat
                const lngBnd = bounds['_northEast'].lng
                const zoom = map.getZoom();
                const lat = map.getCenter().lat;
                const lng = map.getCenter().lng;
                // update endpoint which triggers API call (via useEffect on top)
                dispatch(setEndpoint({type:"trees", lat:lat, lng:lng, latbnd:latBnd, lngbnd:lngBnd}))
            },
        
        });

    // render component based on if there is available data
    if (stateMarker.cities.length > 0) {
        return (
            <MapContainer preferCanvas={true} center={[stateMap.lat, stateMap.lng]} zoom={stateMap.zoom} scrollWheelZoom={true}>
              <GetMapProperties />
              <TileLayer
                attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
              />
              <GetMarkers />
            </MapContainer>
        );
    } else {
        return (
            <MapContainer center={[stateMap.lat, stateMap.lng]} zoom={stateMap.zoom} scrollWheelZoom={true}>
              <TileLayer
                attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
              />
            </MapContainer>
        );
    }

}
4
  • That depends on the number of markers you have. If the number of markers are not too many it is ok to rerender them all. But if they are too many (hundreds or thousands) they take up a lot of memory as they require many dom elements to draw and that is expensive for the browser. It would be nice to provide a small demo to reproduce the issue.
    – kboul
    Commented Dec 9, 2020 at 8:29
  • @kboul Hard to make a demo myself, but the markers are definitely in the hundreds in one screen. I have seen demos of people rendering tens of thousands of data points on a map without issue though. Why is that? Is it an issue with the marker icon? Should I replace the icon with something more efficient? Commented Dec 9, 2020 at 18:24
  • You can use circle markers, canvas option, or marker clusters to render big number of markers. Check here for more details
    – kboul
    Commented Dec 9, 2020 at 19:16
  • I think your rerendering issue is coming from the fact that stateMarkers is a state variable on LeafMap, so when it changes, the whole map rerenders. I would break out GetMarkers into its own component and move all the API and state logic into that, and do not make the rendering of the map conditional on that state variable. Just render GetMarkers no matter what. If stateMarker.cities.length is 0, its fine, it will just render no markers. At least this way, when the api call is run, GetMarkers updates, but not necessarily the whole map Commented Dec 10, 2020 at 5:26

0