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

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='&copy; <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>
    )
}

问题原因

  1. 内部组件动态创建:BoundTracker和HomeButton定义在Map组件内部,每次Map组件渲染时,这两个组件都会被重新定义为全新的组件类型。React会将其视为不同的组件,导致内部的useMapEvents、useMap等Hook的调用顺序在每次渲染时无法保持一致,触发Hook调用顺序错误。
  2. 依赖版本兼容性:更新依赖后,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='&copy; <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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 03:10:29