React检测到ForwardRef(ContainerComponent)的Hook调用顺序变更问题排查
解决React-Leaflet更新依赖后的Hook调用顺序错误
问题描述
使用react-leaflet开发应用,更新所有依赖后出现错误:
React has detected a change in the order of Hooks called by ForwardRef(ContainerComponent)
移除<MapContainer>组件可消除错误,但无法定位ForwardRef组件的渲染位置及Hook调用顺序变更原因。相关组件代码如下:
Map组件
const Map = ({ openModal }) => { const [homeCoords, setHomeCoords] = useState([49.2, -123]); const [bounds, setBounds] = useState({ lat_lower: 48.9, lat_upper: 49.5, lon_left: -123.8, lon_right: -122.2 }); // 获取用户位置 useEffect(() => { if(!window.navigator.geolocation) { return; } window.navigator.geolocation.getCurrentPosition( (res) => { setHomeCoords([res.coords.latitude, res.coords.longitude]); }, () => { console.error('必须允许网站获取位置才能使用此功能。') } ) }, []); // 更新地图边界的辅助函数 const collectBounds = (e) => { const bounds = e.target.getBounds(); const currLatLower = bounds._southWest.lat; const currLatUpper = bounds._northEast.lat; const currLonLeft = bounds._southWest.lng; const currLonRight = bounds._northEast.lng; setBounds({ lat_lower: currLatLower, lat_upper: currLatUpper, lon_left: currLonLeft, lon_right: currLonRight }) } // 监听地图拖拽和缩放更新边界 const BoundTracker = () => { useMapEvents({ dragend: (e) => { collectBounds(e); }, zoomend: (e) => { collectBounds(e); } }) } const HomeButton = () => { const map = useMap(); return ( <div className="btn home" aria-disabled="false" onClick={() => { map.panTo(homeCoords); const bounds = map.getBounds(); setBounds({ lat_lower: bounds._southWest.lat, lat_upper: bounds._northEast.lat, lon_left: bounds._southWest.lng, lon_right: bounds._northEast.lng }) }}> <AiFillHome /> </div> ) } return ( <> <MapContainer className="Map" position={homeCoords} zoom={10}> <TileLayer attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> <HomeButton /> <div className="btn info" onClick={() => openModal("Welcome")}><AiOutlineInfoCircle /></div> <div className="btn legend" onClick={() => openModal("Legend")}><BsMap /></div> <div className="search-btn" onClick={() => return}>搜索此区域</div> <PointClusters /> <BoundTracker /> </MapContainer> </> ) }
PointClusters组件
import { useState } from 'react'; import MarkerClusterGroup from 'react-leaflet-cluster'; import { Marker, Popup } from 'react-leaflet'; import L from 'leaflet'; import './PointClusters.css'; // 测试数据 const testPoints = require('../../../testPoints.json').features; const PointClusters = () => { const [points, setPoints] = useState(testPoints); const newicon = new L.divIcon({ className: "custom-marker", html: "<span class='arrow'></span>" }); const createClusterCustomIcon = function (cluster) { return L.divIcon({ html: `<span>${cluster.getChildCount()}</span>`, className: 'custom-marker-cluster', iconSize: L.point(33, 33, true), }) } return ( <MarkerClusterGroup iconCreateFunction={createClusterCustomIcon} > { points.map((point, i) => { const { name, address, type, numOfReports, url } = point.properties; const coords = [point.geometry.coordinates[1], point.geometry.coordinates[0]]; return ( <Marker position={coords} icon={newicon} key={i}> <Popup> { name === "None" ? null : <> <b>{name}</b> <br /> </> } <strong>地址</strong>: {address} <br /> <strong>类型</strong>: {type} <hr /> 在bedbugregistry.com上至少有 <a href={url}>{numOfReports} 条报告</a> </Popup> </Marker> ) }) } </MarkerClusterGroup> ) }
问题原因
- 内部组件动态创建:
BoundTracker和HomeButton定义在Map组件内部,每次Map组件渲染时,这两个组件都会被重新定义为全新的组件类型。React会将其视为不同的组件,导致内部的useMapEvents、useMap等Hook的调用顺序在每次渲染时无法保持一致,触发Hook调用顺序错误。 - 依赖版本兼容性:更新依赖后,react-leaflet(尤其是v4+版本)对Hook的使用规则更加严格,内部的
ForwardRef(ContainerComponent)是MapContainer的容器组件,子组件的Hook调用不稳定会触发该组件的错误提示。
解决方案
1. 将内部组件移到外部
把BoundTracker和HomeButton从Map组件内部移出,通过props传递需要依赖的状态和方法,避免每次渲染重新创建组件:
// 移到Map组件外部 const BoundTracker = ({ collectBounds }) => { useMapEvents({ dragend: (e) => { collectBounds(e); }, zoomend: (e) => { collectBounds(e); } }) return null; } // 移到Map组件外部 const HomeButton = ({ homeCoords, updateBounds }) => { const map = useMap(); return ( <div className="btn home" aria-disabled="false" onClick={() => { map.panTo(homeCoords); const bounds = map.getBounds(); updateBounds({ lat_lower: bounds._southWest.lat, lat_upper: bounds._northEast.lat, lon_left: bounds._southWest.lng, lon_right: bounds._northEast.lng }) }}> <AiFillHome /> </div> ) } const Map = ({ openModal }) => { const [homeCoords, setHomeCoords] = useState([49.2, -123]); const [bounds, setBounds] = useState({ lat_lower: 48.9, lat_upper: 49.5, lon_left: -123.8, lon_right: -122.2 }); useEffect(() => { if(!window.navigator.geolocation) { return; } window.navigator.geolocation.getCurrentPosition( (res) => { setHomeCoords([res.coords.latitude, res.coords.longitude]); }, () => { console.error('必须允许网站获取位置才能使用此功能。') } ) }, []); const collectBounds = (e) => { const bounds = e.target.getBounds(); const currLatLower = bounds._southWest.lat; const currLatUpper = bounds._northEast.lat; const currLonLeft = bounds._southWest.lng; const currLonRight = bounds._northEast.lng; setBounds({ lat_lower: currLatLower, lat_upper: currLatUpper, lon_left: currLonLeft, lon_right: currLonRight }) } return ( <> <MapContainer className="Map" position={homeCoords} zoom={10}> <TileLayer attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> <HomeButton homeCoords={homeCoords} updateBounds={setBounds} /> <div className="btn info" onClick={() => openModal("Welcome")}><AiOutlineInfoCircle /></div> <div className="btn legend" onClick={() => openModal("Legend")}><BsMap /></div> <div className="search-btn" onClick={() => return}>搜索此区域</div> <PointClusters /> <BoundTracker collectBounds={collectBounds} /> </MapContainer> </> ) }
2. 检查依赖版本兼容性
确保react-leaflet和react-leaflet-cluster的版本与React版本兼容:
- React 18需要react-leaflet v4.x及以上
- 确认react-leaflet-cluster的版本适配当前react-leaflet版本,可查看其npm文档确认兼容范围
3. 遵循Hook使用规则
确保所有Hook调用都在组件的顶层作用域,不要在条件判断、循环或嵌套函数中调用Hook(本问题中主要是组件动态创建导致的Hook身份变化,移到外部即可解决)。
内容的提问来源于stack exchange,提问作者Willey Ohana
相关产品推荐
相关产品推荐

