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

切换标签页后React-Leaflet地图变灰交互报错解决方案

问题现象
  • 开发包含「地址」「地图」两个标签页的对话框组件:地址标签页配置城市名、街道名两个文本输入框;地图标签页基于react-leaflet渲染,支持点击选点、设备自动定位功能
  • 切换标签页后地图变为单调灰色块,缩放操作无法恢复显示
  • 对地图执行点击、缩放等交互时,控制台抛出错误:Uncaught Error: Set map center and zoom first.
  • 异常效果截图:
    地图灰化截图
    控制台报错截图
问题根因
  • MUI的TabPanel默认会卸载非激活状态的标签内容,切换标签时地图组件被销毁后重新挂载,但原有map实例状态未做重置,且whenCreated回调更新状态时覆盖了已存储的position字段,导致地图重挂载时拿不到有效的center和zoom参数
  • 标签切换过程中地图容器尺寸发生变化,Leaflet未感知到容器尺寸变更,无法正确渲染瓦片,出现灰块
  • 事件回调中存在闭包问题,读取到的map实例为旧的无效值,调用地图方法时触发异常
修复方案
  1. 修正地图实例创建时的状态更新逻辑,避免覆盖已存储的位置信息
    将MapContainer的whenCreated属性从原有代码:
whenCreated={map => setMapState({ map })}

修改为:

whenCreated={map => setMapState(prev => ({ ...prev, map }))}
  1. 保留地图标签的DOM挂载状态,避免切换标签时地图实例被销毁
    给地图所在的TabPanel添加keepMounted属性:
<TabPanel value="1" keepMounted>
{/* 原有地图相关代码 */}
</TabPanel>
  1. 监听标签切换事件,切到地图页时主动触发地图尺寸重算,解决灰块问题
    在组件内新增useEffect逻辑:
useEffect(() => {
  if (value === 1 && mapState.map) {
    // 等待DOM渲染完成后重算地图尺寸
    setTimeout(() => {
      mapState.map.invalidateSize();
      // 兜底:如果中心点无效则重置到默认位置
      if (!mapState.position) {
        mapState.map.setView(initializerPos, 15);
      }
    }, 100);
  }
}, [value, mapState.map, mapState.position, initializerPos]);
  1. 修复事件回调的闭包问题,保证操作地图时能拿到有效实例
    将showMyLocation、AddMarkerToClick中读取map实例的逻辑调整为从最新状态中获取,避免使用闭包缓存的旧null值。
原问题实现代码
import * as React from 'react';
import { useEffect, useState } from 'react';

import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import useMediaQuery from '@mui/material/useMediaQuery';
import CloseIcon from '@mui/icons-material/CloseOutlined';
import { Divider, IconButton, InputLabel } from '@mui/material';
import { Box, Grid, Tab, TextField, useTheme } from '@material-ui/core';
import { TabContext, TabList, TabPanel } from '@mui/lab';
import "leaflet/dist/leaflet.css";
import icon from "../../../../Account/components/constants";
import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet'

const useGeoLocation = () => {
    const [location, setLocation] = useState({
        loaded: false,
        coordinates: { lat: "", lng: "" }
    });
    const onSuccess = (location) => {
        setLocation({
            loaded: true,
            coordinates: {
                lat: location.coords.latitude,
                lng: location.coords.longitude,
            }
        });
    };
    const onError = (error) => {
        setLocation({
            loaded: true,
            error,
        });
    };
    useEffect(() => {
        if (!("geolocation" in navigator)) {
            onError({
                code: 0,
                message: "Your device GPS is OFF!",
            });
        }
        navigator.geolocation.getCurrentPosition(onSuccess, onError);

    }, []);

    return location;
}
export default function AddressDialog() {
    const [open, setOpen] = useState(false);
    const theme = useTheme();
    const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
    const [value, setValue] = useState(0);

    const [city, setCity] = useState("");
    const [street, setStreet] = useState();
    
    const [initializerPos,] = useState([40.689247, -74.044502]);
    const [position, setPosition] = useState(initializerPos);
    const [mapState, setMapState] = useState({
        position: position,
        map: null
    });
    const zoom_level = 18;
    const location = useGeoLocation();

    const handleClickOpen = () => {
        setOpen(true);
    };

    const handleClose = () => {
        setOpen(false);
        setValue(0);
    };

    const handleSubmit = () => {
        // 接口更新逻辑
    }

    const showMyLocation = () => {
        if (location.loaded && !location.error) {
            let pos = [location.coordinates.lat, location.coordinates.lng];
            setPosition([location.coordinates.lat, location.coordinates.lng]);
            setMapState((state) => ({
                ...state,
                position: pos,
            }));

            const { map } = mapState;
            if (map) {
                map.flyTo(pos, zoom_level);
            }
        } else {
            if (location.error) {
                alert(location.error.message)
            }
            else {
                alert("Problem in loading curent location!")
            }
        }
    };
    function AddMarkerToClick() {
        const [markers, setMarkers] = useState([]);
        useMapEvents({
            click(e) {
                const newMarker = e.latlng
                setMarkers([...markers, newMarker]);
                setPosition([e.latlng.lat, e.latlng.lng]);
                setMapState((state) => ({
                    ...state,
                    position: newMarker,
                }));
                const { map } = mapState;
                if (map) {
                    map.flyTo(newMarker, zoom_level);
                }
            },
        });

        return null
    };
    
    return (
        <div dir="ltr">
            <Button onClick={handleClickOpen} variant="contained" type="button" aria-label="Edit Info" fullWidth size="small">
                Edit Address Info
            </Button>
            <Dialog fullScreen={fullScreen} open={open} aria-labelledby="responsive-dialog-title">
                <DialogTitle>
                    <IconButton onClick={handleClose} aria-label="Close Dialog">
                        <CloseIcon fontSize="medium" />
                    </IconButton>
                </DialogTitle>
                <Divider />

                <DialogTitle>Edit Address</DialogTitle>
                <DialogContent id ="dialogContent" >
                    <DialogContentText>
                        In this section you are able to edit your address info
                    </DialogContentText>
                    <TabContext value={value.toString()} >
                        <Box >
                            <TabList
                                onChange={(event, newValue) => { setValue(parseInt(newValue, 10));}}
                                aria-label="address-map-tab">
                                <Tab label="Address" value="0" />
                                <Tab label="Map" value="1" />
                            </TabList>
                        </Box>
                        
                        <TabPanel value="0">
                            <Grid container spacing={theme.spacing(0)}>
                                <Grid item xs={12} sm={6}>
                                    <TextField value={city} onChange={(e) => setCity(e.target.value)} margin="normal" variant="outlined"
                                        required
                                        fullWidth
                                        type="text"
                                        name="area"
                                        id="area"
                                        label={"city"}
                                        placeholder={"ex: New York"}
                                    />
                                </Grid>
                                <Grid item xs={12} sm={6}>
                                    <TextField
                                        value={street}
                                        onChange={(e) => setStreet(e.target.value)}
                                        margin="normal"
                                        variant="outlined"
                                        required
                                        fullWidth
                                        type="text"
                                        name="street"
                                        id="street"
                                        label={"Streen Name"}
                                        placeholder={"ex: wall street"}
                                    />
                                </Grid>
                            </Grid>
                        </TabPanel>
                        <TabPanel value="1">
                            <Grid container>
                                <div style={{
                                    marginLeft: "auto", 
                                    marginRight: "auto",
                                    width: "100%"
                                }}>
                                    <InputLabel>
                                        Your location in map: 
                                    </InputLabel>
                                        <MapContainer
                                            center={mapState.position}
                                            zoom ={15}
                                            scrollWheelZoom
                                            style={{
                                                height: fullScreen ? 200 : 350,
                                                width: "100%",
                                                textAlign: "center",
                                                marginLeft: "auto",
                                                marginRight: "auto",
                                                marginTop: theme.spacing(1)
                                            }}
                                            whenCreated={map => setMapState({ map })}
                                        >
                                            <TileLayer
                                                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
                                            />
                                            <AddMarkerToClick />
                                            {
                                                position && (
                                                    <Marker position={position} icon={icon}></Marker>
                                                )
                                            }
                                        </MapContainer>
                                        <Button variant="outlined" color="primary" size="large" onClick={showMyLocation}>
                                            Here!
                                        </Button>
                                </div>
                            </Grid>
                        </TabPanel>
                    </TabContext>
                </DialogContent>
                <DialogActions style={{ marginLeft: theme.spacing(2), marginRight: theme.spacing(2) }}>
                    <Grid container direction="row" spacing={1}>
                        <Grid item container xs={4} dir="left" justifyContent="flex-end">
                            <Button variant="contained" type="button" color="error" fullWidth
                                name="cancel-btn" onClick={handleClose}
                            >
                                Cancel
                            </Button>
                        </Grid>
                        <Grid item container xs={8} >
                            <Button variant="contained" type="button" color="primary" fullWidth
                                name="submit-btn" onClick={handleSubmit} >
                                Save
                            </Button>
                        </Grid>
                    </Grid>
                </DialogActions>
            </Dialog>
        </div>
    );
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 03:48:23