NextJs集成Google Maps时表单输入致地图重载丢失选中位置
问题分析
问题出在两个核心点:
- 父组件中
handleLocationSelect函数会在每次组件渲染(比如输入框修改触发setEventData)时重新创建,导致子组件LocationSelector的useEffect因依赖onLocationSelect变化而重新执行,地图被反复初始化,之前选中的标记丢失。 LocationSelector组件没有保存选中位置的状态,也未接收父组件传递的已选位置,无法在地图重新渲染后恢复标记。
解决方案
1. 父组件优化:缓存位置选择回调函数
在父组件createForm中用useCallback包裹handleLocationSelect,确保函数引用稳定,避免子组件不必要的重渲染。
2. 子组件改造:支持接收已选位置并持久化标记
- 给
LocationSelector添加currentLocationprops,接收父组件的已选位置。 - 让地图只初始化一次,通过状态保存标记实例,监听
currentLocation变化自动更新标记位置。
修改后的代码
父组件 createForm
'use client'; import Header from "@/app/components/Header"; import { useState, useCallback } from 'react'; import { Textarea } from "@nextui-org/react"; import { createEvent } from "@/app/conexion-sw/event_requests"; import LocationSelector from "@/app/components/LocationSelector"; import { set } from "mongoose"; export default function createForm() { const [location, setLocation] = useState({ lat: 0, lng: 0 }); const [eventData, setEventData] = useState({ title: '', date: '', description: '', images: [ "url_imagen_3", "url_imagen_4" ], category: [ "Concierto" ], participants: [], price: 0, isFree: true, capacity: 0, address: '', location: { lat: 0, lng: 0 }, }); const handleSubmit = async (event) => { event.preventDefault(); const formData = new FormData(event.target); const data = Object.fromEntries(formData); data.location = location; console.log('Datos del formulario:', data); createEvent(data).then((res) => { console.log("Respuesta del servidor", res); }).catch((err) => { console.error("Error al crear el evento", err); }); }; const handleInputChange = (e) => { const { name, value } = e.target; setEventData({ ...eventData, [name]: value, }); }; // 用useCallback缓存函数,避免每次渲染重新创建 const handleLocationSelect = useCallback((selectedLocation) => { console.log('Ubicación seleccionada frontend:', selectedLocation); setLocation(selectedLocation); }, [setLocation]); const handleFileChange = (e) => { const { name, files } = e.target; console.log('Imágenes seleccionadas:', files); }; return ( <div className="flex flex-col h-full bg-white-mode-background dark:bg-dark-mode-background items-center justify-center"> <Header /> <div className="h-fit w-fit rounded border bg-white-mode-bg-login border-white-mode-border text-white-mode-text dark: bg-dark-mode-bg-login dark:text-dark-mode-text dark:border-dark-mode-border items-center mt-16"> <form onSubmit={handleSubmit} className="bg-transparent p-4"> <h1 className="text-2xl font-bold mb-4 text-center">Crear Evento</h1> <div className="mb-4"> <label htmlFor="title" className="block mb-2"> Nombre del evento: </label> <input type="text" id="title" name="title" className="bg-transparent p-2 rounded border w-full border-white-mode-border dark:border-dark-mode-border" onChange={handleInputChange} required /> </div> <div className="mb-4"> <label htmlFor="date" className="block mb-2"> Fecha del evento: </label> <input type="datetime-local" id="date" name="date" className="bg-transparent p-2 rounded border w-full border-white-mode-border dark:border-dark-mode-border" onChange={handleInputChange} required /> </div> <div className="mb-4"> <label htmlFor="description" className="block mb-2"> Descripción del evento: </label> <Textarea id="description" name="description" label="Descripción" variant="bordered" placeholder="Ingresa la descripción del evento" className="max-w-xs bg-transparent p-2 rounded border w-full border-white-mode-border dark:border-dark-mode-border" onChange={handleInputChange} required /> </div> <div className="mb-4"> <label htmlFor="images" className="block mb-2"> Imágenes del evento: </label> <input type="file" multiple accept="image/*" className="bg-transparent p-2 rounded border w-full border-white-mode-border dark:border-dark-mode-border" /> </div> {/* 传递当前选中位置给子组件 */} <LocationSelector onLocationSelect={handleLocationSelect} currentLocation={location} /> <div className="mb-4"> <button type="submit" className="bg-blue-500 text-white p-2 rounded w-full"> Crear Evento </button> </div> </form> </div> </div> ) }
子组件 LocationSelector
// components/LocationSelector.js import React, { useState, useEffect, useRef } from 'react'; import { Loader } from '@googlemaps/js-api-loader'; const LocationSelector = ({ onLocationSelect, currentLocation }) => { const [map, setMap] = useState(null); const [marker, setMarker] = useState(null); // 保存标记实例 const mapCenter = { lat: -3.9964339532493978, lng: -79.20091423951207 }; const mapZoom = 14; const mapRef = useRef(null); // 用ref获取地图容器,替代getElementById // 初始化地图,只执行一次 useEffect(() => { if (!mapRef.current) return; const loader = new Loader({ apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API, version: 'weekly', }); loader.load().then(() => { const newMap = new window.google.maps.Map(mapRef.current, { center: mapCenter, zoom: mapZoom, }); setMap(newMap); // 地图点击事件 newMap.addListener('click', (event) => { const selectedLocation = event.latLng.toJSON(); updateMarker(event.latLng); onLocationSelect(selectedLocation); }); // 加载时如果有已选位置,设置标记 if (currentLocation.lat !== 0 && currentLocation.lng !== 0) { updateMarker(new window.google.maps.LatLng(currentLocation)); } }); // 清理函数:销毁地图实例 return () => { if (map) { window.google.maps.event.clearInstanceListeners(map); } }; }, []); // 更新标记位置的通用函数 const updateMarker = (location) => { if (marker) { marker.setPosition(location); } else { const newMarker = new window.google.maps.Marker({ position: location, map: map, }); setMarker(newMarker); } // 地图自动移动到标记位置 map.panTo(location); }; // 监听父组件传递的位置变化,更新标记 useEffect(() => { if (!map || !currentLocation || (currentLocation.lat === 0 && currentLocation.lng === 0)) return; const latLng = new window.google.maps.LatLng(currentLocation); updateMarker(latLng); }, [map, currentLocation]); return <div ref={mapRef} style={{ height: '400px', width: '100%' }} />; }; export default LocationSelector;
修改说明
- 父组件用
useCallback缓存handleLocationSelect,保证函数引用稳定,避免子组件重复初始化地图。 - 子组件使用
useRef获取地图容器,比getElementById更符合React的组件化逻辑。 - 地图初始化逻辑只执行一次,避免重复加载导致的标记丢失。
- 通过状态保存标记实例,配合
updateMarker函数统一管理标记的创建和更新。 - 新增监听
currentLocation的useEffect,父组件位置变化时自动同步地图标记。
内容的提问来源于stack exchange,提问作者Andres Macas
相关产品推荐
相关产品推荐

