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

NextJs集成Google Maps时表单输入致地图重载丢失选中位置

问题分析

问题出在两个核心点:

  1. 父组件中handleLocationSelect函数会在每次组件渲染(比如输入框修改触发setEventData)时重新创建,导致子组件LocationSelector的useEffect因依赖onLocationSelect变化而重新执行,地图被反复初始化,之前选中的标记丢失。
  2. LocationSelector组件没有保存选中位置的状态,也未接收父组件传递的已选位置,无法在地图重新渲染后恢复标记。
解决方案

1. 父组件优化:缓存位置选择回调函数

在父组件createForm中用useCallback包裹handleLocationSelect,确保函数引用稳定,避免子组件不必要的重渲染。

2. 子组件改造:支持接收已选位置并持久化标记

  • 给LocationSelector添加currentLocation props,接收父组件的已选位置。
  • 让地图只初始化一次,通过状态保存标记实例,监听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;
修改说明
  1. 父组件用useCallback缓存handleLocationSelect,保证函数引用稳定,避免子组件重复初始化地图。
  2. 子组件使用useRef获取地图容器,比getElementById更符合React的组件化逻辑。
  3. 地图初始化逻辑只执行一次,避免重复加载导致的标记丢失。
  4. 通过状态保存标记实例,配合updateMarker函数统一管理标记的创建和更新。
  5. 新增监听currentLocation的useEffect,父组件位置变化时自动同步地图标记。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 00:36:02