React StrictMode下OpenLayers添加标记时重复图层报错求助
在React StrictMode下解决OpenLayers重复添加图层的问题
问题描述
使用版本:"ol": "^6.14.1",在React项目的OpenLayers地图中添加标记时,每次点击地图都会触发错误:
Uncaught AssertionError: Assertion failed.
错误来自代码行map.addLayer(newMarkersLayer);,具体提示为:
Duplicate item added to a unique collection. For example, it may be that you tried to add the same layer to a map twice. Check for calls to map.addLayer() or other places where the map's layer collection is modified.
问题由React StrictMode导致,关闭严格模式可正常运行,但希望保留StrictMode,修复useEffect写法问题。
问题根源
- React StrictMode在开发环境会双重执行useEffect,导致地图点击事件被重复绑定,每次点击时同一个
newMarkersLayer会被尝试添加两次,触发OpenLayers的唯一集合校验。 - 用
useState存储OpenLayers实例(地图、图层)会受React状态管理干扰,容易出现重复创建或引用问题。
解决方案
修改思路
- 用
useRef存储OpenLayers实例(地图、图层、数据源),避免React状态更新的干扰 - 在useEffect中添加初始化判断,确保实例只创建一次
- 点击事件中先检查图层是否已存在,再执行添加操作
- 添加清理函数,避免内存泄漏
修改后的完整代码
import { useState, useEffect, useRef } from 'react'; import classes from './MapUI.module.css'; import { drawerActions } from '../Rooms/Drawers/drawerSlice'; import 'ol/ol.css'; import { Map, View, Overlay, Feature } from 'ol'; import Point from 'ol/geom/Point'; import { Vector as VectorLayer } from 'ol/layer'; import VectorSource from 'ol/source/Vector'; import { fromLonLat, toLonLat } from 'ol/proj'; import { toStringHDMS } from 'ol/coordinate'; import TileLayer from 'ol/layer/Tile'; import OSM from 'ol/source/OSM'; import PopUp from './PopUp'; import { useDispatch } from 'react-redux'; export default function MapUI() { const dispatch = useDispatch(); const mapRef = useRef(); const popup = useRef(); const [coordinates, setCoordinates] = useState(''); // 用useRef存储OpenLayers实例,避免React状态管理干扰 const mapRefInst = useRef(null); const newMarkersLayerRef = useRef(null); const newMarkerSourceRef = useRef(null); const overlayRef = useRef(null); const closePopup = () => { if (mapRefInst.current && overlayRef.current) { overlayRef.current.setPosition(undefined); // 移除图层前先检查是否存在 if (mapRefInst.current.getLayers().getArray().includes(newMarkersLayerRef.current)) { mapRefInst.current.removeLayer(newMarkersLayerRef.current); } } }; useEffect(() => { // 初始化地图实例,确保只创建一次 if (!mapRefInst.current) { mapRefInst.current = new Map({ target: mapRef.current, layers: [ new TileLayer({ source: new OSM(), }), new VectorLayer({ properties: { name: 'existingMarkers' }, source: new VectorSource(), }), ], view: new View({ center: fromLonLat([26.08, 44.46]), zoom: 15, minZoom: 10, maxZoom: 20, }), }); } // 初始化标记图层和数据源 if (!newMarkersLayerRef.current) { newMarkerSourceRef.current = new VectorSource(); newMarkersLayerRef.current = new VectorLayer({ properties: { name: 'newMarkers' }, source: newMarkerSourceRef.current, }); } // 初始化弹窗Overlay if (!overlayRef.current) { overlayRef.current = new Overlay({ element: popup.current, id: 'map-popup', autoPan: { animation: { duration: 250, }, }, }); mapRefInst.current.addOverlay(overlayRef.current); } // 定义点击事件处理函数 const handleMapClick = (evt) => { const map = mapRefInst.current; const layer = newMarkersLayerRef.current; const source = newMarkerSourceRef.current; // 检查图层是否已添加,未添加则执行添加 if (!map.getLayers().getArray().includes(layer)) { map.addLayer(layer); } dispatch(drawerActions.closeDrawer()); // 创建新标记要素并更新数据源 const markerFeature = new Feature({ geometry: new Point(evt.coordinate), name: '', }); source.clear(); source.addFeature(markerFeature); setCoordinates(toStringHDMS(toLonLat(evt.coordinate))); overlayRef.current.setPosition(evt.coordinate); }; // 添加点击事件监听 mapRefInst.current.on('singleclick', handleMapClick); // 清理函数:移除事件监听,避免内存泄漏 return () => { if (mapRefInst.current) { mapRefInst.current.un('singleclick', handleMapClick); mapRefInst.current.setTarget(null); } }; }, [dispatch]); // 空依赖确保只执行一次,StrictMode下的重复执行会被清理函数抵消 return ( <> <div style={{ height: '100%', width: '100%' }} ref={mapRef} className='map-container' /> <div id='map-popup' className={classes['ol-popup']} ref={popup}> <PopUp coordinates={coordinates} closePopup={closePopup} /> </div> </> ); }
关键修改点说明
- 用
useRef替代useState存储地图、图层、数据源等实例,这些对象不需要触发React渲染,用ref更稳定 - 初始化时添加存在性判断,确保StrictMode下实例不会被重复创建
- 点击事件中先校验图层是否已存在于地图集合,避免重复添加
- 通过清空数据源再添加新要素的方式更新标记,避免直接修改已有Feature的引用问题
- 添加useEffect清理函数,移除事件监听并解绑地图target,防止内存泄漏
内容的提问来源于stack exchange,提问作者GeorgeMet
相关产品推荐
相关产品推荐

