在React函数组件中,Hooks带来了更直观的状态与副作用管理方式,但useEffect、useMemo等API依赖一个叫依赖数组(dependency array)的第二个参数来控制执行时机。不少人在使用时不小心写出了无限循环,或者因为漏写依赖导致界面没有正确刷新。要理解这些错误,先得搞清楚React在每次渲染后是如何比对依赖项的:它采用Object.is逐位比较上一次与这一次的依赖值,只要有一个不等就重新执行副作用。

为什么依赖数组会引发无限循环
无限循环最常见的场景是在useEffect内部调用了会修改状态的函数,而该状态(或由此派生的新对象)又被放进了依赖数组。React执行副作用后状态改变,组件重新渲染,新的依赖值被判定为不同,于是副作用再次执行,如此往复。比如下面这段代码,每次 effect 运行都把 count 加一,而 count 本身就在依赖里,必然导致死循环。
import React, { useState, useEffect } from 'react';
function BadCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
// 每次执行都把 count 改变,而 count 在依赖中
setCount(count + 1);
}, [count]);
return <div>{count}</div>;
}
另一个隐蔽的无限循环来源是依赖项里放了字面量对象或函数。由于每次渲染这些引用类型都会生成新地址,Object.is比较永远返回false,effect便频繁触发。要解决这个问题,应当避免把每次渲染都新建的引用直接写进数组,或者通过useRef、useCallback稳定引用。同时,如果副作用只想在挂载时运行一次,依赖数组应传空数组,但要确保内部逻辑不依赖会变化的外部变量,否则就会变成遗漏依赖的问题。
遗漏依赖项会带来哪些隐蔽故障
与无限循环相反,有些开发者为了“让effect少跑几次”而故意删减依赖,结果组件拿到了过时的闭包数据。例如一个effect依赖了外部的userId去请求接口,但依赖数组只写了空,那么userId变化后请求并不会重发,页面始终展示旧用户的信息。这种bug不会抛错,却让产品表现完全不符合预期,排查起来非常费劲。
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
// 错误:缺少 userId 依赖
fetch('/api/user/' + userId)
.then(res => res.json())
.then(setData);
}, []); // 只挂载一次,userId 变了也不重新请求
return <div>{data ? data.name : '加载中'}</div>;
}
React官方提供的 eslint-plugin-react-hooks 会静态检查并提示缺失的依赖,这是避免遗漏的最实用工具。当确实需要在依赖中排除某项时,应当先把该值用useRef存起来,在effect里读取ref.current,或者把相关逻辑抽成纯函数并用useCallback包裹后再放入依赖。这样既能通过 lint 检查,也保证行为正确。需要强调的是,遗漏依赖不是性能优化手段,而是一种错误写法。
稳定依赖与拆分逻辑的实践方案
要同时躲开无限循环和遗漏依赖,核心思路是让依赖数组里的每一项都具备稳定的引用或原始值,并且完整覆盖effect所用到的外部变量。对于函数类型的依赖,使用useCallback记忆化;对于对象,考虑把内部字段拆成原始值依赖,或用useMemo生成稳定对象。下面示例展示了如何用useCallback修复函数依赖导致的重复执行。
import React, { useState, useEffect, useCallback } from 'react';
function GoodList({ query }) {
const [list, setList] = useState([]);
const fetchList = useCallback(() => {
fetch('/api/list?q=' + query)
.then(res => res.json())
.then(setList);
}, [query]); // fetchList 的引用随 query 稳定变化
useEffect(() => {
fetchList();
}, [fetchList]); // 只依赖稳定的函数
return <ul>{list.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}
当副作用里需要用到频繁变化但不想触发effect的值,比如计时器ID、上一次滚动位置,可使用useRef。ref的.current修改不会引发渲染,也不会被依赖比对捕获,非常适合存放可变中间态。此外,把重计算逻辑移出渲染主体,用useMemo缓存结果,也能减少不必要的effect执行。总之,写Hooks时先问自己:这个effect用到了哪些外部值?它们是否稳定?答案清晰了,依赖数组自然就不会出错。
最后补充一点关于自定义Hooks的约定:如果你封装了一个内部使用useEffect的逻辑,应当把内部依赖管理好,并对调用方暴露最简接口。调用方在组装依赖时,同样遵循原始值直接写、引用值先记忆化的原则。团队内部统一开启严格模式的Hooks lint规则,能从工程层面杜绝绝大多数无限循环与遗漏依赖引发的线上故障。
React_HooksuseEffectdependency_array修改时间:2026-08-17 19:24:28