在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
相关产品推荐
相关产品推荐

