使用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> ))} </> ); }
问题原因
错误根源在于状态更新与组件渲染的循环依赖:
- 取消过滤器时,之前被卸载的
AdvancedMarker重新挂载,触发ref回调调用setMarkers更新状态 markers状态变化触发useEffect,调用clusterer的clearMarkers和addMarkers方法- 这个操作可能导致地图组件重新渲染,进而再次触发
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> ))} </> ); }
关键改动说明
- 简化过滤器状态:直接在Home组件中通过
filterCapital计算markerFilter,去掉多余的useEffect和状态变量 - 改用ref存储标记:使用
allMarkersRef存储所有标记实例,避免状态更新触发的不必要渲染 - 基于过滤器更新clusterer:在
useEffect中直接根据当前过滤器筛选标记,更新clusterer,而非依赖标记状态变化 - 始终渲染所有标记:不再通过条件渲染卸载/挂载
AdvancedMarker,而是通过clusterer控制哪些标记显示,避免ref回调反复触发状态更新
内容的提问来源于stack exchange,提问作者dan
相关产品推荐
相关产品推荐

