Next.js项目中Mapbox实例重复输出问题求助
问题描述
在Next.js项目中集成Mapbox时,控制台无报错,但打印Map实例时会输出两次:首次为null,第二次才是正常的Map实例。尝试过修改Webpack配置处理Worker文件,未解决问题。
完整代码
import React, { useEffect, useRef, useState } from 'react'; // eslint-disable-line import/no-webpack-loader-syntax // @ts-ignore import mapboxgl from '!mapbox-gl'; import 'mapbox-gl/dist/mapbox-gl.css'; import Image from 'next/image'; import moment from 'moment'; import { MAP_STYLES, CONFIG_MAP } from '@/components/MapContainer/config'; import styles from './styles.module.scss'; interface MapProps { config: any; heatmap: any; } interface MapInfo { sourceId: string; layerId: any; data: any; config: any; urlObj: any; } export const MapContainer: React.FC<MapProps> = ({ config, heatmap }) => { const defaultCenter = [-84.45, 33.76]; const defaultZoom = 10; const center = heatmap?.center || defaultCenter; const mapContainer = useRef<any>(null); const map = useRef<mapboxgl.Map | any>(null); const [lng, setLng] = useState(center[0]); const [lat, setLat] = useState(center[1]); const [zoom, setZoom] = useState(config?.zoom_level || defaultZoom); const [mapStyle, setMapStyle] = useState(MAP_STYLES[0].style); const [mapInfo, setMapInfo] = useState<MapInfo[]>([]); const [isMenuOpen, setIsMenuOpen] = useState(false); useEffect(() => { console.log('Map instance:', map.current); if (mapContainer.current && !map.current) { mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN as string; map.current = new mapboxgl.Map({ container: mapContainer.current, style: mapStyle, center: center, zoom: zoom, }); map.current.on('load', () => { heatmap?.data?.forEach((data: any, index: any) => { loadGeoJson(data, index); }); map.current?.addControl( new mapboxgl.GeolocateControl({ positionOptions: { enableHighAccuracy: true, }, trackUserLocation: true, showUserHeading: true, }), ); map.current?.addControl(new mapboxgl.NavigationControl()); }); map.current.on('move', () => { if(map.current) { setLng(map.current.getCenter().lng); setLat(map.current.getCenter().lat); setZoom(map.current.getZoom()); } }); } return () => { if (map.current) { map.current.remove(); } }; }, []); useEffect(() => { if (map.current) { map.current.setStyle(mapStyle); } }, [mapStyle]); const toggleMenu = () => { setIsMenuOpen(!isMenuOpen); }; const handleStyleChange = (style: string) => { setMapStyle(style); setIsMenuOpen(false); }; const parseDate = (inputDate: any) => { const cleanedDate = inputDate.replace(/<br\/>/g, '').trim(); return moment(cleanedDate, 'Do MMM YYYY').format('YYYY-MM-DD'); }; const parseTime = (inputTime: any) => { return moment(inputTime, 'h A').format('HH'); }; const loadGeoJson = async (config: any, layerId: any) => { try { console.log('Loading GeoJSON data:', config?.url); const response = await fetch(config?.url); if (!response.ok) { new Error(`Error loading GeoJSON data: ${response.statusText}`); } const data = await response.json(); const {uniqueSourceId} = configureMap(map, data, layerId, config); const _mapInfo = [...mapInfo, { sourceId: uniqueSourceId, layerId: layerId, data: data, config: config, urlObj: config?.url, }]; setMapInfo(_mapInfo); } catch (error) { console.error('Error loading GeoJSON data:', error); } }; const configureMap = (map: mapboxgl.Map, data: any, layerId: any, conf: any) => { const uniqueLayerId = `cluster-layer-${layerId}`; const uniqueSourceId = `cluster-source-${layerId}`; config.functions.forEach((func: any) => { const {name, layerType = "clustered", args} = func; // @ts-ignore CONFIG_MAP[name]({args, map, data, layerType, layerId, uniqueLayerId, uniqueSourceId}, conf); }); return { uniqueSourceId, uniqueLayerId, }; }; return ( <main className={styles['custom-map-container']}> <div className='relative w-full h-full'> <div ref={mapContainer} className='w-full h-full'/> <div className='absolute bottom-4 right-4 flex items-center'> <div className='relative'> <button className={`bg-[#b0b0b0] text-white p-1 flex items-center space-x-2 flex-col ${ isMenuOpen ? 'rounded-tr-md rounded-br-md' : 'rounded' }`} onClick={toggleMenu} style={{fontSize: '10px'}} > <span> <Image src={MAP_STYLES[0].icon} alt={MAP_STYLES[0].name} className='w-10 h-10 cursor-pointer' onClick={() => handleStyleChange(MAP_STYLES[0].style)} /> </span> <span className='text-center'>Streets</span> {isMenuOpen && ( <div className='absolute right-full top-0 mr-2 bg-[#b0b0b0] p-1 flex space-x-2 rounded-tl-md rounded-bl-md'> {MAP_STYLES.slice(1).map(({style, name, icon}) => ( <div key={style} className='flex flex-col items-center'> <span className='flex items-center justify-center w-10 h-10'> <Image src={icon} alt={name} className='w-full h-full object-contain cursor-pointer' onClick={() => handleStyleChange(style)} /> </span> <span className='text-white text-center text-[10px]'>{name}</span> </div> ))} </div> )} </button> </div> </div> <div className='ml-auto align-items-center flex text-sm mt-1 mb-3'> Longitude: {lng?.toFixed(2)} | Latitude: {lat?.toFixed(2)} | Zoom: {zoom?.toFixed(2)} </div> </div> </main> ); };
解决方案
1. 理解React Strict Mode的触发逻辑
Next.js默认启用React Strict Mode,在开发环境下会强制触发两次组件挂载(包括useEffect执行两次),目的是检测潜在的副作用问题。这就是你看到两次console.log的核心原因:第一次挂载时map.current未初始化,所以打印null;第二次挂载时,因为map.current已经被赋值,所以打印正常实例,但不会重复创建地图。
2. 优化初始化逻辑,避免无效日志和重复操作
添加一个initialized标志位,确保地图只初始化一次,同时过滤掉无效的null日志:
// 在组件内新增一个ref用于标记是否已初始化 const initialized = useRef(false); useEffect(() => { // 仅在未初始化时执行逻辑 if (initialized.current) return; if (mapContainer.current && !map.current) { mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN as string; map.current = new mapboxgl.Map({ container: mapContainer.current, style: mapStyle, center: center, zoom: zoom, }); console.log('Map instance:', map.current); // 移到这里,只打印有效实例 // 原有的map.on('load')、addControl等逻辑保持不变 // 标记为已初始化 initialized.current = true; } return () => { if (map.current) { map.current.remove(); // 重置标志位,确保组件卸载后重新挂载时能再次初始化 initialized.current = false; } }; }, [center, zoom, mapStyle, heatmap]); // 补全所有依赖项,避免stale closure
3. 修复状态更新的Stale问题
在loadGeoJson中,更新mapInfo时使用函数式更新,避免依赖旧的状态值:
const loadGeoJson = async (config: any, layerId: any) => { try { console.log('Loading GeoJSON data:', config?.url); const response = await fetch(config?.url); if (!response.ok) { throw new Error(`Error loading GeoJSON data: ${response.statusText}`); // 修复throw错误的写法 } const data = await response.json(); const {uniqueSourceId} = configureMap(map.current, data, layerId, config); // 使用函数式更新获取最新状态 setMapInfo(prevMapInfo => [...prevMapInfo, { sourceId: uniqueSourceId, layerId: layerId, data: data, config: config, urlObj: config?.url, }]); } catch (error) { console.error('Error loading GeoJSON data:', error); } };
4. 修正configureMap的参数错误
configureMap中误用了外部的config变量,应该使用传入的conf参数:
const configureMap = (map: mapboxgl.Map, data: any, layerId: any, conf: any) => { const uniqueLayerId = `cluster-layer-${layerId}`; const uniqueSourceId = `cluster-source-${layerId}`; // 把config.functions改为conf.functions conf.functions.forEach((func: any) => { const {name, layerType = "clustered", args} = func; // @ts-ignore CONFIG_MAP[name]({args, map, data, layerType, layerId, uniqueLayerId, uniqueSourceId}, conf); }); return { uniqueSourceId, uniqueLayerId, }; };
5. 关于Webpack Worker配置的补充说明
Mapbox GL JS的Worker加载问题在Next.js中通常可以通过动态导入解决,若仍有Worker加载异常,可尝试用动态导入替代直接引入:
// 替换原来的import mapboxgl from '!mapbox-gl' import dynamic from 'next/dynamic'; const MapboxGL = dynamic(() => import('mapbox-gl'), { ssr: false, }); // 使用时确保在客户端渲染后初始化 useEffect(() => { const initMap = async () => { const mapboxgl = await MapboxGL; mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN as string; // ... 创建地图逻辑 }; initMap(); }, []);
内容的提问来源于stack exchange,提问作者Ayushi jain
相关产品推荐
相关产品推荐

