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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 00:14:52