Google Maps React API多边形onSave延迟更新问题求助
问题
我正在使用Google Maps React API开发多边形渲染与编辑功能,目前已实现以下功能:
- 成功渲染后端返回的GeoJSON数据;
- 可识别多边形顶点,弹出菜单删除顶点后,多边形能即时更新,状态变量也同步变化。
但遇到onSave函数的问题:每次mouseup触发onSave时,控制台日志可正常输出,但新的GeoJSON结构无法即时同步到状态,需触发第二次才会更新。现附上代码:
export function MapComponents() { const t = useTranslations(); const { fieldDataV2, setPolygon, polygon } = useFieldDetailsContext(); const { map } = useLayoutProvider(); const [text, setText] = useState(''); const [counter, setCounter] = useState(0); const currentPolyCoordinates = polygon?.geometry?.coordinates; const currentFeature = map?.data.getFeatureById('test'); const area = (currentPolyCoordinates && calculateArea(currentPolyCoordinates)) || fieldDataV2?.boundary?.properties?.area; const setFeatureStyle = ( feature: google.maps.Data.Feature, ): google.maps.Data.StyleOptions => { return { fillOpacity: feature.getProperty('fillOpacity') as number, // Set fill opacity fillColor: feature.getProperty('fillColor') as string, // Set fill color for the polygon strokeColor: feature.getProperty('color') as string, // Set stroke color based on the 'color' property strokeWeight: feature.getProperty('strokeWeight') as number, zIndex: feature.getProperty('zIndex') as number, // Set stroke weight as needed editable: true, clickable: true, }; }; const contentString = `<div id="context-menu-delete-icon" style={{cursor: "pointer"}} >Löschen</div`; const infowindow = new google.maps.InfoWindow({ ariaLabel: 'Delete-Menu', content: contentString, disableAutoPan: true, }); // This useEffect handles the initial drawing of the polygon on the map useEffect(() => { if (map) { // Add GeoJSON boundary data to the map fieldDataV2 && map.data.addGeoJson(fieldDataV2?.boundary); fieldDataV2 && setPolygon(fieldDataV2.boundary); // Set style for the added feature map.data.setStyle(setFeatureStyle); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [map, fieldDataV2]); const onEdit = (event: google.maps.PolyMouseEvent) => { const clickedPoint = event.latLng; const lngLatArray = clickedPoint && [ clickedPoint.lng(), clickedPoint.lat(), ]; // Check if the clicked point matches any polygon vertices and their lenght is greater then 3 const isMatch = currentPolyCoordinates && lngLatArray && isPointInsideArrays(currentPolyCoordinates, lngLatArray); // Open the info window for deletion if there are enough points and we have a vertix match if (isMatch) { infowindow.open({ map }); infowindow.setPosition(clickedPoint); infowindow.open({ map: map, shouldFocus: false }); const coordinatesToWorkWith = lngLatArray && currentPolyCoordinates && removeFirstPointFromArray(lngLatArray, currentPolyCoordinates); // Remove the matched point and format the result for geometry modification const filteredAndConvertedCoordinates = coordinatesToWorkWith && lngLatArray && filterAndConvertCoordinates(coordinatesToWorkWith, lngLatArray); // Add a handler to the infowindow as soon as it gets domready google.maps.event.addListener(infowindow, 'domready', function () { const contextDeleteMenu = document.getElementById( 'context-menu-delete-icon', ); const infowindowDefaultCloseIcon = document.getElementsByClassName('gm-ui-hover-effect'); if (infowindowDefaultCloseIcon instanceof HTMLCollection) { const infoWindowCloseX = infowindowDefaultCloseIcon[0] as HTMLElement; infoWindowCloseX.style.display = 'none'; } if (contextDeleteMenu) { contextDeleteMenu.style.cursor = 'pointer'; //Close the infowindow and set the new geometrie to the feature contextDeleteMenu.addEventListener('click', () => { infowindow.close(); filteredAndConvertedCoordinates && currentFeature?.setGeometry( new google.maps.Data.Polygon(filteredAndConvertedCoordinates), ); currentFeature?.toGeoJson((data) => { setPolygon(data as GeoJsonFeaturePolygon); setText(JSON.stringify(data) + counter); }); }); } }); } }; const onSave = () => { currentFeature?.toGeoJson((data) => { setPolygon(data as GeoJsonFeaturePolygon); setText(JSON.stringify(data) + counter); setCounter(counter + 1); }); }; useEffect(() => { if (map) { // Close the info window anytime a user clicks anywhere on the map const mapClickHandler = google.maps.event.addListener( map, 'click', () => { infowindow.close(); }, ); // Handles click events on vertices const vertexClickHandler = map.data.addListener('click', onEdit); // Update the polygon to the state and remove the infowindow const saveGeometryHandler = map.data.addListener('mouseup', onSave); // Cleanup function removes all handlers to prevent multiple initiations on re-renders return () => { mapClickHandler.remove(); vertexClickHandler.remove(); saveGeometryHandler.remove(); }; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [map, polygon]); }
问题排查与修复
1. onSave函数状态更新延迟原因
- 闭包旧值问题:
onSave中使用的counter是组件渲染时的闭包值,React状态更新异步,setCounter(counter + 1)的新值要到下一次渲染才生效,导致setText无法同步显示最新状态,造成setPolygon未更新的错觉。 - Stale Feature引用:
currentFeature在组件顶层直接获取,不会随多边形修改自动更新,可能拿到旧实例。
修复后的onSave函数
const onSave = () => { // 实时获取最新的Feature实例 const latestFeature = map?.data.getFeatureById('test'); latestFeature?.toGeoJson((data) => { setPolygon(data as GeoJsonFeaturePolygon); // 使用函数式更新获取最新counter值 setCounter(prevCounter => { const newCounter = prevCounter + 1; setText(JSON.stringify(data) + newCounter); return newCounter; }); }); };
整体代码优化建议
1. 避免Stale状态/引用
- 不在组件顶层缓存
currentFeature,而是在onEdit、onSave等函数内部实时获取,确保拿到最新实例。 - 依赖前一次状态更新时,使用函数式更新(如
setCounter(prev => prev + 1)),规避闭包旧值问题。
2. 优化事件监听器管理
- 将地图事件监听器的绑定放在仅依赖
map的useEffect中,避免因polygon变化频繁解绑/重绑。 infowindow的domready监听器会重复绑定,建议在添加前移除旧监听器,或放在useEffect中统一管理并清理。
3. 优化初始渲染逻辑
- 初始添加GeoJSON时,先检查是否已存在ID为
test的Feature,避免重复添加:
useEffect(() => { if (map && fieldDataV2) { const existingFeature = map.data.getFeatureById('test'); if (!existingFeature) { map.data.addGeoJson(fieldDataV2.boundary); } setPolygon(fieldDataV2.boundary); map.data.setStyle(setFeatureStyle); } }, [map, fieldDataV2]);
4. 类型安全与可读性提升
contentString中的style是字符串模板,改用普通CSS语法:style="cursor: pointer"。- 给
isPointInsideArrays、removeFirstPointFromArray等工具函数添加类型定义。 - 将
infowindow的创建放在useEffect中,避免每次渲染重复创建:
const [infowindow, setInfowindow] = useState<google.maps.InfoWindow | null>(null); useEffect(() => { if (map) { const infoWindowInstance = new google.maps.InfoWindow({ ariaLabel: 'Delete-Menu', content: `<div id="context-menu-delete-icon" style="cursor: pointer">Löschen</div>`, disableAutoPan: true, }); setInfowindow(infoWindowInstance); return () => { infoWindowInstance.close(); }; } }, [map]);
5. 减少不必要渲染
- 用
useMemo缓存currentPolyCoordinates,避免每次渲染重复计算:
const currentPolyCoordinates = useMemo(() => polygon?.geometry?.coordinates, [polygon]);
- 若
text仅用于调试,生产环境可直接移除,减少状态更新触发的渲染。
内容的提问来源于stack exchange,提问作者SirThanksALot
相关产品推荐
相关产品推荐

