You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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写法问题。

问题根源

  1. React StrictMode在开发环境会双重执行useEffect,导致地图点击事件被重复绑定,每次点击时同一个newMarkersLayer会被尝试添加两次,触发OpenLayers的唯一集合校验。
  2. 用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 10:25:34