React Hook useEffect依赖缺失:添加函数依赖触发无限循环的解决方案
解决React数据获取Hook的无限循环与依赖警告问题
问题根源
loadData(即setEnabled)每次组件渲染会生成新引用,直接加入父组件useEffect依赖会触发重复执行useData内部的fetcheffect错误依赖了data和error,而请求过程中会修改这两个状态,导致effect反复触发形成无限循环- 路由切换会触发组件重渲染,进一步加剧循环问题
修复后的useData Hook
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; // 定义你的Row类型 interface Row { // 示例字段,根据实际需求修改 id: number; content: string; } // 假设这是你的数据获取函数 const fetchFunction = async (): Promise<Row[]> => { // 替换为实际的API请求逻辑 const res = await fetch('/api/data'); return res.json(); }; const useData = () => { const [error, setError] = useState<string | null>(null); const [rows, setRows] = useState<Row[]>([]); const [enabled, setEnabled] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false); const [isFetching, setIsFetching] = useState<boolean>(false); const isCancelled = useRef(false); // 直接通过useMemo计算数据长度,无需额外状态 const count = useMemo(() => rows.length, [rows]); const data = useMemo(() => rows, [rows]); // 核心修复:仅依赖enabled控制请求触发 useEffect(() => { const fetchData = async () => { if (isCancelled.current) return; setIsLoading(true); setIsFetching(true); setError(null); try { const fetchedRows = await fetchFunction(); // 检查是否已取消,避免更新卸载组件的状态 if (!isCancelled.current) { setRows(fetchedRows); } } catch (err) { if (!isCancelled.current) { const e = err as Error; setError(e.message); } } finally { if (!isCancelled.current) { setIsLoading(false); setIsFetching(false); } } }; isCancelled.current = false; if (enabled) { fetchData(); // 请求触发后重置enabled,防止重复执行 setEnabled(false); } // 组件卸载时标记取消请求 return () => { isCancelled.current = true; }; }, [enabled]); // 用useCallback稳定loadData的引用,消除依赖警告 const loadData = useCallback((shouldLoad: boolean) => { setEnabled(shouldLoad); }, []); return { data, count, error, isLoading, loadData }; }; export default useData;
父组件正确使用方式
import useData from './useData'; import { useEffect } from 'react'; // 如果需要处理路由变化,引入useLocation // import { useLocation } from 'react-router-dom'; const DataPage = () => { const { data, count, error, isLoading, loadData } = useData(); // const location = useLocation(); // 初始化加载数据,现在可以安全添加loadData到依赖 useEffect(() => { loadData(true); }, [loadData]); // 如果需要路由切换时重新加载数据,添加路由依赖 // useEffect(() => { // loadData(true); // }, [loadData, location.pathname]); return ( <div className="data-container"> {isLoading && <div className="loading">加载中...</div>} {error && <div className="error">出错了:{error}</div>} <div className="count">共 {count} 条数据</div> <ul className="data-list"> {data.map(row => ( <li key={row.id}>{row.content}</li> ))} </ul> </div> ); }; export default DataPage;
关键修复点说明
- 移除冗余状态:用
useMemo直接计算count,省去额外的useEffect更新逻辑 - 修正effect依赖:仅保留
enabled作为触发请求的唯一条件,切断状态更新与effect执行的循环链 - 请求安全处理:添加
isCancelled检查,防止在组件卸载后更新状态引发警告 - 稳定函数引用:用
useCallback包裹loadData,确保父组件useEffect依赖不会频繁变化,消除ESLint警告同时避免无限循环 - 路由适配:如果需要路由切换时重新获取数据,直接监听路由状态即可,不会触发额外循环
内容的提问来源于stack exchange,提问作者Simon B. Nielsen
相关产品推荐
相关产品推荐

