I’m using OpenLayers inside the React environment (background info). I want to control what the map shows with React – using context and other components. For example, zoom in to the map if something is selected in the react components. I am using the latest OpenLayers (10.1) and React(18.3.1), and the problem I have is that the moveEnd event triggers too many times. For a simple window resize, I’m getting 20+ move-end triggers:
My problem is that I’m trying to load new data for the map, and this triggers too many requests against the backend. So I thought that perhaps Moveend was not the best event, but other similar events, like Dragend, also triggered the same number of events.
I’m also thinking, that perhaps in React environment the way I do things is not the best, as map resizes trigger data (re)loads, but data loads also trigger re-renders.
The image is taken before I simplified my whole setup, but currently it’s like that:
'use client';
import { useRef, useState, useCallback, useContext } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Feature } from 'ol';
import { fromLonLat, toLonLat } from 'ol/proj';
import { getBottomLeft, getTopRight } from 'ol/extent';
import { Point } from 'ol/geom';
import { Icon, Style } from 'ol/style';
import { fetcher } from 'utils/axios';
import { MapContext } from 'context/ResizeableMapContext';
import OSMTileLayer from 'components/atoms/OSMTileLayer';
import MapComponent from 'components/atoms/Map';
import VectorLayer from 'components/atoms/VectorLayer';
const AuthenticatedMap = function () {
const { mapArea, mapObject, setMapObject } = useContext(MapContext);
const [mapBox, setMapBox] = useState({});
const onMoveEnd = useCallback((e) => {
console.log('ON MOVE END TRIGGERS');
const { map } = e;
const extent = map.getView().calculateExtent(map.getSize());
const bottomLeft = toLonLat(getBottomLeft(extent));
const topRight = toLonLat(getTopRight(extent));
setMapBox({
tr: {
lon: topRight[0],
lat: topRight[1],
},
bl: {
lon: bottomLeft[0],
lat: bottomLeft[1],
},
});
}, []);
const getUrlAndKey = () => {
const baseUrl = `/api/ships/visible-ships/?`;
const baseKey = 'k:';
if ('tr' in mapBox && 'bl' in mapBox) {
const {
tr: { lon: trLon, lat: trLat },
bl: { lon: blLon, lat: blLat },
} = mapBox;
return [
`${baseUrl}min_lon=${blLon}&min_lat=${blLat}&max_lon=${trLon}&max_lat=${trLat}`,
`${blLon}&${blLat}&${trLon}&${trLat}`,
];
}
return [baseUrl, baseKey];
};
const [url, key] = getUrlAndKey();
const { isPending, isError, data } = useQuery({
queryKey: ['publiclyAvailableShips', key],
refetchInterval: 1000 * 60,
queryFn: () => fetcher(url),
enabled: 'tr' in mapBox && 'bl' in mapBox,
});
const features = [];
if (!isPending && !isError && data.length) {
data.forEach((ship) => {
const feature = new Feature({
geometry: new Point(fromLonLat([ship.latest_position.lon, ship.latest_position.lat])),
});
feature.setStyle(
new Style({
image: new Icon({
src:
"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' " +
"width='32' height='32' viewBox='0 0 24 24' fill='%23e5f2fe' stroke='%23000' " +
"stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath " +
"d='M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7Z'/%3e%3ccircle " +
"cx='12' cy='9' r='3'/%3e%3c/svg%3e",
className: 'shadow-xl',
scale: 1,
}),
}),
);
feature.set('name', ship.name);
features.push(feature);
});
}
return (
<MapComponent
zoom={10}
center={fromLonLat([24.637865, 59.511668])}
width={mapArea}
map={mapObject}
setMap={setMapObject}
moveEndCallback={onMoveEnd}
>
<OSMTileLayer />
<VectorLayer>{features}</VectorLayer>
</MapComponent>
);
};
export default AuthenticatedMap;
Now, my question is, how can I stop excessive move-end triggers? I mean, I move the window size, like 100 pixels, in 1-2 seconds. That should not trigger the callback 20+ times. Is the solution really to implement my own delay that makes sure the callback is only triggered once per 250ms?
Edit: My current solution is to implement a delay in MapComponent
, but is that really the best solution here?
mapObj.on('moveend', (event) => {
// Clear previous timeout to reset the delay
if (moveEndTimeoutRef.current) {
clearTimeout(moveEndTimeoutRef.current);
}
// Set a new timeout to call the moveEndCallback after 100ms
moveEndTimeoutRef.current = setTimeout(() => {
moveEndCallback(event);
}, 150); // 150ms delay
});
All the best,
Alan