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

使用Marker Clusterer实现动态Google Maps标记时出现最大更新深度超限错误

问题描述

尝试为Google Maps标记搭配Marker Clusterer实现过滤器功能:

  • 初始状态下地图显示所有标记
  • 勾选复选框过滤器后,仅渲染/添加标记子集
  • 取消勾选时,对应标记应重新显示

初始状态和勾选过滤器时功能正常,但取消勾选时触发Maximum update depth exceeded(最大更新深度超限)错误。

原代码

"use client";
import { AdvancedMarker, APIProvider, Map, useMap } from "@vis.gl/react-google-maps"
import { MarkerClusterer } from "@googlemaps/markerclusterer";
import type { Marker } from "@googlemaps/markerclusterer";
import { useEffect, useState, useRef } from "react";

const MAP_ID = "xxx";
const API_KEY = "xxx";

type Point = google.maps.LatLngLiteral & {
    key: string,
    city: string
    isCapital: boolean,
};

const locations: Point[] = [
    {
        key: "0",
        city: "Austin",
        lat: 30.241241516441292,
        lng: -97.73378248930749,
        isCapital: true,
    },
    {
        key: "1",
        city: "San Antonio",
        lat: 29.517273972261396,
        lng: -98.44789382382311,
        isCapital: false,
    },
];

type MarkerFilter = "capital" | "all";

export default function Home() {
    const center = { lat: 30.241241516441292, lng: -97.73378248930749 };
    const zoom = 6;
    const [filterCapital, setFilterCapital] = useState(false);
    const [markerFilter, setMarkerFilter] = useState<MarkerFilter>("all");
    useEffect(() => {
        let val: MarkerFilter = "all";
        if (filterCapital) {
            val = "capital";
        }
        setMarkerFilter(() => val);
    }, [filterCapital]);
    return (
        <main>
            <div>
                <label>Capitals</label>
                <input
                    type="checkbox"
                    name="capital"
                    checked={filterCapital}
                    onChange={(e) => setFilterCapital(() => e.target.checked)}
                />
            </div>
            <div style={{ height: "100vh", width: "100%" }}>
                <APIProvider apiKey={API_KEY}>
                    <Map mapId={MAP_ID} defaultCenter={center} defaultZoom={zoom}>
                        <Markers points={locations} filter={markerFilter}/>
                    </Map>
                </APIProvider>
            </div>
        </main>
    );
}

type Props = { points: Point[], filter: MarkerFilter};

const Markers = ({ points, filter }: Props) => {
    const map = useMap();
    const [markers, setMarkers] = useState<{ [key: string]: Marker }>({});
    const clusterer = useRef<MarkerClusterer | null>(null);
    useEffect(() => {
        if (!map) return;
        if (!clusterer.current) {
            clusterer.current = new MarkerClusterer({ map });
        }
    }, [ map ]);
    useEffect(() => {
        clusterer.current?.clearMarkers();
        clusterer.current?.addMarkers(Object.values(markers));
    }, [ markers ]);
    const setMarkerRef = (marker: Marker | null, key: string) => {
        if (marker && markers[key]) return;
        if (!marker && !markers[key]) return;
        setMarkers((prev) => {
            if (marker) {
                return { ...prev, [key]: marker };
            } else {
                const newMarkers = { ...prev };
                delete newMarkers[key];
                return newMarkers;
            }
        });
    }
    return (
        <>
            {points.map((point) => (
                (filter == "all" || (filter == "capital" && point.isCapital)) && <AdvancedMarker
                    key={point.key}
                    position={point}
                    ref={(marker) => setMarkerRef(marker, point.key)}>
                    <div>{point.city}</div>
                </AdvancedMarker>
            ))}
        </>
    );
}

问题原因

错误根源在于状态更新与组件渲染的循环依赖:

  1. 取消过滤器时,之前被卸载的AdvancedMarker重新挂载,触发ref回调调用setMarkers更新状态
  2. markers状态变化触发useEffect,调用clusterer的clearMarkers和addMarkers方法
  3. 这个操作可能导致地图组件重新渲染,进而再次触发AdvancedMarker的ref回调,形成无限更新循环,最终触发最大更新深度超限错误

解决方案

优化标记管理逻辑,避免通过状态收集标记实例,改用ref存储所有标记,直接根据过滤器结果更新clusterer:

"use client";
import { AdvancedMarker, APIProvider, Map, useMap } from "@vis.gl/react-google-maps"
import { MarkerClusterer } from "@googlemaps/markerclusterer";
import type { Marker } from "@googlemaps/markerclusterer";
import { useEffect, useState, useRef } from "react";

const MAP_ID = "xxx";
const API_KEY = "xxx";

type Point = google.maps.LatLngLiteral & {
    key: string,
    city: string
    isCapital: boolean,
};

const locations: Point[] = [
    {
        key: "0",
        city: "Austin",
        lat: 30.241241516441292,
        lng: -97.73378248930749,
        isCapital: true,
    },
    {
        key: "1",
        city: "San Antonio",
        lat: 29.517273972261396,
        lng: -98.44789382382311,
        isCapital: false,
    },
];

type MarkerFilter = "capital" | "all";

export default function Home() {
    const center = { lat: 30.241241516441292, lng: -97.73378248930749 };
    const zoom = 6;
    const [filterCapital, setFilterCapital] = useState(false);
    // 直接计算过滤器状态,去掉多余的useEffect和状态变量
    const markerFilter = filterCapital ? "capital" : "all";

    return (
        <main>
            <div>
                <label>Capitals</label>
                <input
                    type="checkbox"
                    name="capital"
                    checked={filterCapital}
                    onChange={(e) => setFilterCapital(e.target.checked)}
                />
            </div>
            <div style={{ height: "100vh", width: "100%" }}>
                <APIProvider apiKey={API_KEY}>
                    <Map mapId={MAP_ID} defaultCenter={center} defaultZoom={zoom}>
                        <Markers points={locations} filter={markerFilter}/>
                    </Map>
                </APIProvider>
            </div>
        </main>
    );
}

type Props = { points: Point[], filter: MarkerFilter};

const Markers = ({ points, filter }: Props) => {
    const map = useMap();
    const clusterer = useRef<MarkerClusterer | null>(null);
    // 用ref存储所有标记实例,避免状态更新触发的不必要渲染
    const allMarkersRef = useRef<{ [key: string]: Marker }>({});

    // 初始化clusterer
    useEffect(() => {
        if (!map) return;
        if (!clusterer.current) {
            clusterer.current = new MarkerClusterer({ map });
        }
    }, [map]);

    // 根据过滤器更新clusterer中的标记
    useEffect(() => {
        if (!clusterer.current) return;
        
        // 筛选符合当前过滤器的标记
        const filteredMarkers = points
            .filter(point => filter === "all" || point.isCapital)
            .map(point => allMarkersRef.current[point.key])
            .filter(marker => marker); // 确保标记存在
        
        clusterer.current.clearMarkers();
        clusterer.current.addMarkers(filteredMarkers);
    }, [filter, points]);

    const setMarkerRef = (marker: Marker | null, key: string) => {
        if (marker) {
            allMarkersRef.current[key] = marker;
        } else {
            delete allMarkersRef.current[key];
        }
    };

    return (
        <>
            {/* 始终渲染所有标记,通过clusterer控制显示/隐藏,而非卸载组件 */}
            {points.map((point) => (
                <AdvancedMarker
                    key={point.key}
                    position={point}
                    ref={(marker) => setMarkerRef(marker, point.key)}>
                    <div>{point.city}</div>
                </AdvancedMarker>
            ))}
        </>
    );
}

关键改动说明

  1. 简化过滤器状态:直接在Home组件中通过filterCapital计算markerFilter,去掉多余的useEffect和状态变量
  2. 改用ref存储标记:使用allMarkersRef存储所有标记实例,避免状态更新触发的不必要渲染
  3. 基于过滤器更新clusterer:在useEffect中直接根据当前过滤器筛选标记,更新clusterer,而非依赖标记状态变化
  4. 始终渲染所有标记:不再通过条件渲染卸载/挂载AdvancedMarker,而是通过clusterer控制哪些标记显示,避免ref回调反复触发状态更新

内容的提问来源于stack exchange,提问作者dan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 21:00:56