React中useEffect依赖props引发无限循环的通用解决方案
以下示例代码触发了React Hook的依赖警告:
import { useState, useEffect } from "react"; const MyComponent = (props) => { const { names, indexes, onIndexesChange } = props; const [sortDir, setSortDir] = useState(1); useEffect(() => { const newIndexes = [0, 1, 2]; newIndexes.sort((i1, i2) => { return sortDir * names[i1].localeCompare(names[i2]); }); onIndexesChange(newIndexes); }, [sortDir, onIndexesChange]); return ( <p> <button onClick={() => setSortDir(-sortDir)}>Click</button> <br /> {indexes.map((index) => names[index])} </p> ); }; export default function App() { const names = ["Newton", "Einstein", "Pascal"]; const [indexes, setIndexes] = useState([0, 1, 2]); // 实际代码中indexes在多个组件间共享,因此定义在MyComponent外部 return ( <div className="App"> <MyComponent names={names} indexes={indexes} onIndexesChange={setIndexes} /> </div> ); }
警告内容:
React Hook useEffect has a missing dependency: 'names'. Either include it or remove the dependency array.
若将names添加到依赖数组中,在React 18.2.0版本会出现无限循环(React 18.0.0版本无此问题),原因是names在每次渲染时都会被重新定义。需要实现无论names以何种方式声明(例如作为state或const变量)都能正常运行的方案。
解决方案1:用useMemo缓存names(静态/低频次变化场景)
在父组件中通过useMemo缓存names,保证其引用稳定,之后在MyComponent的useEffect中正常添加names到依赖数组即可:
父组件修改:
export default function App() { // 依赖为空数组,仅初始化一次,引用稳定 const names = useMemo(() => ["Newton", "Einstein", "Pascal"], []); const [indexes, setIndexes] = useState([0, 1, 2]); return ( <div className="App"> <MyComponent names={names} indexes={indexes} onIndexesChange={setIndexes} /> </div> ); }
MyComponent的useEffect修改:
useEffect(() => { const newIndexes = [0, 1, 2]; newIndexes.sort((i1, i2) => { return sortDir * names[i1].localeCompare(names[i2]); }); onIndexesChange(newIndexes); }, [sortDir, onIndexesChange, names]);
这种方式完全符合React的依赖检查规范,同时避免了因names引用频繁变化导致的无限循环。
解决方案2:用useRef同步最新names(高频次变化场景)
如果不想修改父组件,或者names会频繁变化,可以在MyComponent内部用useRef实时同步最新的names值,在useEffect中直接使用ref的当前值:
const MyComponent = (props) => { const { names, indexes, onIndexesChange } = props; const namesRef = useRef(names); const [sortDir, setSortDir] = useState(1); // 每次props更新时同步ref值 useEffect(() => { namesRef.current = names; }, [names]); useEffect(() => { const newIndexes = [0, 1, 2]; newIndexes.sort((i1, i2) => { // 使用ref中存储的最新names return sortDir * namesRef.current[i1].localeCompare(namesRef.current[i2]); }); onIndexesChange(newIndexes); }, [sortDir, onIndexesChange]); return ( <p> <button onClick={() => setSortDir(-sortDir)}>Click</button> <br /> {indexes.map((index) => names[index])} </p> ); };
这种方式无需依赖names的引用稳定性,无论names是const还是state,都能获取到最新值,同时避免了将names加入useEffect依赖数组导致的循环问题。
解决方案3:结合函数式状态更新与useCallback(逻辑内聚场景)
利用React状态更新的函数式写法,将排序逻辑封装到onIndexesChange的回调中,同时用useCallback稳定回调引用:
const MyComponent = (props) => { const { names, indexes, onIndexesChange } = props; const [sortDir, setSortDir] = useState(1); // 用useCallback缓存排序回调,依赖sortDir、names和onIndexesChange const handleSort = useCallback(() => { onIndexesChange(prevIndexes => { const newIndexes = [...prevIndexes]; newIndexes.sort((i1, i2) => { return sortDir * names[i1].localeCompare(names[i2]); }); return newIndexes; }); }, [sortDir, names, onIndexesChange]); useEffect(() => { handleSort(); }, [handleSort]); return ( <p> <button onClick={() => setSortDir(prev => -prev)}>Click</button> <br /> {indexes.map((index) => names[index])} </p> ); };
如果父组件的names是动态的,可结合方案1的useMemo稳定其引用;如果不想依赖names引用,也可结合方案2的useRef方式。这种写法让排序逻辑更内聚,同时符合React的依赖规范。
内容的提问来源于stack exchange,提问作者Holt

