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

在React Leaflet中结合useContext与Leaflet Routing Machine报错求助

问题:react-leaflet controlComponent中使用useContext触发更新时出现Hook顺序错误

我在react-leaflet的controlComponent里使用useContext Hook,触发上下文更新函数时出现了Hook顺序错误,因为Leaflet Routing Machine的需求使用了该组件,相关代码和错误信息如下:

MainBoard.tsx

export const CartographyContext: React.Context<CartographyContextType> = React.createContext<CartographyContextType>({ positions: [] });
...

const routeSummaryValueContext = React.useMemo(
  () => ({ routeSummary, setRouteSummary }),
  [routeSummary]
);

const elevationProfileValueContext = React.useMemo(
  () => ({ elevationProfile, setElevationProfile }),
  [elevationProfile]
);

........

<CartographyContext.Provider value={{ positions, elevationProfileValueContext, routeSummaryValueContext, positionsValueContext, addPosition, changePosition }}>
.........
<RoutingMachine
   orsOptions={{
        ....
   }} />
..........
</CartographyContext.Provider>

RoutingMachine.tsx

const CreateRoutineMachineLayer = (props: any) => {
    const geoService = new GeoLocalisationService();
    const cartographyContext: CartographyContextType = React.useContext<CartographyContextType>(CartographyContext);
    const [routes, setRoutes] = React.useState<any[]>();

    React.useEffect(() => {
        if (routes) {
             //引发错误的代码行
cartographyContext.elevationProfileValueContext.setElevationProfile(geoService.getElevationProfile(decodePolyline(routes[0].geometry, true)));
            const summary: RouteSummary = {
                ascent: routes[0].routeSummary.ascent,
                descent: routes[0].routeSummary.descent,
                distance: routes[0].routeSummary.distance,
                estimatedDuration: routes[0].routeSummary.duration
            }
            cartographyContext.routeSummaryValueContext.setRouteSummary(summary);
        }
    }, [routes]);

    const { orsOptions } = props;

    const instance = L.Routing.control({
        router: new OpenRouteRouter(orsOptions),
        lineOptions: {
            styles: [{ color: "#3933ff", weight: 4 }],
            extendToWaypoints: true,
            missingRouteTolerance: 0
        },
        routeWhileDragging: true,
        autoRoute: true,
        geocoder: new geocoder.Geocoder(),

    }).on('routesfound', (e) => {
        setRoutes(e.routes);
    });

    useMapEvents({
        click: (e: L.LeafletMouseEvent) => {
            if (instance.getWaypoints().length === 2 && instance.getWaypoints()[0].latLng == null) {
                instance.spliceWaypoints(0, 1, new L.Routing.Waypoint(e.latlng, null, {}));
            } else if (instance.getWaypoints().length === 2 && instance.getWaypoints()[1].latLng == null) {
                instance.spliceWaypoints(1, 1, new L.Routing.Waypoint(e.latlng, null, {}));
            } else {
                instance.spliceWaypoints(instance.getWaypoints().length, 0, new L.Routing.Waypoint(e.latlng, null, {}));
            }
        }
    });


    return instance;
};

const RoutingMachine = createControlComponent(CreateRoutineMachineLayer);

错误信息

React has detected a change in the order of Hooks called by ForwardRef(LeafComponent). This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks

   Previous render            Next render
   ------------------------------------------------------
1. useContext                 useContext
2. useRef                     useRef
3. useContext                 useRef
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


..............
Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

解决方法

这个错误的核心是react-leaflet的createControlComponent生成的控件创建函数,不属于React的渲染流程,不能在里面直接使用useContext、useState、useEffect等React Hook——Leaflet控件的生命周期和React组件不兼容,会打乱Hook的调用顺序,触发规则校验错误。

按以下步骤修复:

1. 拆分逻辑:把React Hook移到外层组件

不要在传给createControlComponent的CreateRoutineMachineLayer里写Hook逻辑,而是把这部分放到一个标准的React组件中,再通过props把需要的更新函数传进来。

2. 修改MainBoard.tsx,传递上下文更新函数

在CartographyContext的Provider内部,直接把需要的setElevationProfile和setRouteSummary作为props传给RoutingMachine:

// MainBoard.tsx中,Provider内部
const { setElevationProfile } = elevationProfileValueContext;
const { setRouteSummary } = routeSummaryValueContext;

<RoutingMachine
  orsOptions={{ .... }}
  setElevationProfile={setElevationProfile}
  setRouteSummary={setRouteSummary}
/>

3. 重构RoutingMachine.tsx

重新组织代码,让React Hook都在标准组件顶层调用,Leaflet控件只负责实例化和事件监听:

import { createControlComponent, useMapEvents } from 'react-leaflet';

const RoutingMachine = ({ orsOptions, setElevationProfile, setRouteSummary }) => {
  const geoService = new GeoLocalisationService();
  const [routes, setRoutes] = React.useState<any[]>();
  const controlRef = React.useRef<L.Routing.Control | null>(null);

  // 处理路由数据更新的逻辑,放在React组件顶层
  React.useEffect(() => {
    if (!routes) return;
    
    setElevationProfile(geoService.getElevationProfile(decodePolyline(routes[0].geometry, true)));
    const summary: RouteSummary = {
      ascent: routes[0].routeSummary.ascent,
      descent: routes[0].routeSummary.descent,
      distance: routes[0].routeSummary.distance,
      estimatedDuration: routes[0].routeSummary.duration
    };
    setRouteSummary(summary);
  }, [routes, setElevationProfile, setRouteSummary]);

  // 创建Leaflet控件的函数,仅负责实例化和绑定事件
  const createControl = () => {
    const control = L.Routing.control({
      router: new OpenRouteRouter(orsOptions),
      lineOptions: {
        styles: [{ color: "#3933ff", weight: 4 }],
        extendToWaypoints: true,
        missingRouteTolerance: 0
      },
      routeWhileDragging: true,
      autoRoute: true,
      geocoder: new geocoder.Geocoder(),
    }).on('routesfound', (e) => {
      setRoutes(e.routes);
    });
    controlRef.current = control;
    return control;
  };

  // 地图点击事件处理,在React组件层使用useMapEvents
  useMapEvents({
    click: (e: L.LeafletMouseEvent) => {
      if (!controlRef.current) return;
      const waypoints = controlRef.current.getWaypoints();
      
      if (waypoints.length === 2 && !waypoints[0].latLng) {
        controlRef.current.spliceWaypoints(0, 1, new L.Routing.Waypoint(e.latlng, null, {}));
      } else if (waypoints.length === 2 && !waypoints[1].latLng) {
        controlRef.current.spliceWaypoints(1, 1, new L.Routing.Waypoint(e.latlng, null, {}));
      } else {
        controlRef.current.spliceWaypoints(waypoints.length, 0, new L.Routing.Waypoint(e.latlng, null, {}));
      }
    }
  });

  // 创建并返回Leaflet控件组件
  const LeafletRoutingControl = createControlComponent(createControl);
  return <LeafletRoutingControl />;
};

export default RoutingMachine;

关键注意事项

  • 绝对不要在createControlComponent的回调函数里用React Hook:这个函数是Leaflet调用的,不在React的渲染周期内,Hook必须在React组件的顶层调用才能保证顺序正确。
  • 用useRef保存Leaflet控件实例:避免每次渲染都重新创建控件,同时方便在React事件回调中操作控件。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 02:55:21