React项目怎么配ESLint的exhaustive-deps规则?
本文详细介绍在React项目中配置ESLint的exhaustive-deps规则的具体步骤,涵盖旧版.eslintrc与ESLint 9+ Flat Config的配置方法、自定义Hook支持参数additionalHooks,以及遇到依赖警告时的四种规范修复范式。
exhaustive-deps 规则的核心路径非常直接:安装官方提供的 eslint-plugin-react-hooks 插件,并在 ESLint 配置文件中声明该插件并开启 react-hooks/exhaustive-deps 规则。该规则用于静态检查 useEffect、useCallback 与 useMemo 的依赖数组完整性,防止因遗漏依赖项而触发陈旧闭包(Stale Closure)。本文将逐步讲解在旧版 ESLint 与 ESLint 9+ Flat Config 下的具体配置方式、自定义 Hook 的扩展检查及正确的代码修复范式。
一、准备工作:安装依赖插件
exhaustive-deps 规则并不是 ESLint 核心包自带的,而是内置在 React 官方维护的 eslint-plugin-react-hooks 插件中。在配置之前,必须确保项目中已经安装了该插件以及支持当前项目的 ESLint 基础依赖。
在项目的根目录下打开终端,根据使用的包管理器执行如下安装命令(作为开发依赖):
# 使用 npm
npm install -D eslint-plugin-react-hooks eslint
# 使用 yarn
yarn add -D eslint-plugin-react-hooks eslint
# 使用 pnpm
pnpm add -D eslint-plugin-react-hooks eslint
eslint-config-next 或 @vitejs/plugin-react 附带的规则集中。在重新安装前,可先检查 package.json 的 devDependencies,确认是否已有 eslint-plugin-react-hooks。
二、配置核心规则:两种主流 ESLint 架构
从 ESLint 8.57+ 和 ESLint 9 开始,官方全面转向扁平化配置体系(Flat Config,即 eslint.config.js 或 eslint.config.mjs)。我们需要根据项目现行的配置文件格式选择对应的配置语法。
1. 传统配置方式(.eslintrc.js / .eslintrc.json)
如果项目根目录下存在 .eslintrc.js、.eslintrc.cjs 或 .eslintrc.json,则采用旧版配置结构。
在 .eslintrc.js 中添加配置:
module.exports = {
// 继承推荐配置(最省心的方式,包含 rules-of-hooks 与 exhaustive-deps)
extends: [
'eslint:recommended',
'plugin:react-hooks/recommended'
],
// 或者采用手动声明插件与规则的方式
plugins: [
'react-hooks'
],
rules: {
// 强制遵循 Hooks 的调用规则(只能在顶层和函数组件/自定义Hook中调用)
'react-hooks/rules-of-hooks': 'error',
// 检查 Effect / Callback / Memo 依赖项完整性
// 可选级别: 'warn'(推荐平滑过渡)或 'error'(严格CI流水线)
'react-hooks/exhaustive-deps': 'warn'
}
};
2. ESLint 9+ 扁平化配置(eslint.config.js / eslint.config.mjs)
在 ESLint 9.x 及现代脚手架中,配置统一放在 eslint.config.js(ESM 格式)中,直接导入插件对象并进行规则映射:
import js from '@eslint/js';
import reactHooks from 'eslint-plugin-react-hooks';
export default [
js.configs.recommended,
{
files: ['**/*.{js,jsx,ts,tsx}'],
plugins: {
'react-hooks': reactHooks,
},
rules: {
// 开启 Hooks 核心调用检查
'react-hooks/rules-of-hooks': 'error',
// 开启依赖项检查
'react-hooks/exhaustive-deps': 'warn',
},
},
];
'warn'。如果直接设为 'error',在重构旧项目时可能会导致数十处代码在编译期同时拦截;待团队熟悉规则且历史遗留问题修复完毕后,可将其收紧为 'error' 纳入 CI 门禁。
三、高级配置:通过 additionalHooks 支持自定义 Hook
默认情况下,exhaustive-deps 规则只检查 React 官方原生的三个 Hook:useEffect、useCallback 和 useMemo。
如果团队在业务中封装了类似于 useUpdateEffect、useIsomorphicLayoutEffect、useDebounceEffect 或自定义的数据请求 Hook,ESLint 默认不会去扫描它们的依赖参数。可以通过为规则传入配置对象并指定 additionalHooks 正则表达式来启用检查:
rules: {
'react-hooks/exhaustive-deps': [
'warn',
{
// 传入匹配自定义 Hook 名称的正则表达式(字符串格式)
// 例如同时对 useUpdateEffect、useCustomEffect 生效
additionalHooks: '(useUpdateEffect|useCustomEffect|useDebouncedCallback)'
}
]
}
additionalHooks 仅适用于**其参数签名与 useEffect 或 useCallback 保持一致**(即第一个参数为回调函数,第二个参数为依赖数组)的 Hook。如果自定义 Hook 的依赖项位于第三个参数,该规则无法正确解析,不应纳入匹配。
四、规则原理解析:为什么必须配 exhaustive-deps?
有些开发者在刚接触 React Hooks 时,遇到黄色波浪线往往倾向于使用 // eslint-disable-next-line react-hooks/exhaustive-deps 进行忽略,或者故意在依赖数组中留空 [] 来模拟“仅在挂载时运行一次”。这种做法极易引发**陈旧闭包(Stale Closure)**。
在 React 的渲染模型中,每一次 Render 都是一个拥有独立 Props 和 State 的快照环境。在函数组件内部声明的事件函数与 Effect 回调,会通过 JavaScript 闭包捕获本次渲染时的局部变量。
如果 Effect 内部使用了状态 count,但没有将其列入依赖数组,当组件后续因为 count 递增而重新渲染时,React 不会重新创建 Effect。此时,Effect 内部的回调函数引用的依旧是初次挂载时捕获的旧版本 count=0。这种数据不一致往往不会抛出任何运行时异常,但会在异步请求、定时器或事件监听中表现为数据无法更新、状态倒退或逻辑死锁。
五、四种典型依赖警告与正确重构范式
当 exhaustive-deps 报出黄色下划线警告时,绝大多数情况代表组件的设计存在可优化的数据流。以下是四种最常见的场景及标准修复方式:
场景 1:在 Effect 内部使用的辅助函数
问题现象:在组件顶层定义了一个请求数据的函数 fetchData,并在 useEffect 中调用它,ESLint 提示需要将 fetchData 加入依赖数组。如果直接加进去,因为函数在每次渲染时都是新对象,会导致死循环请求。
// ❌ 错误做法:直接放入依赖导致死循环,或使用 eslint-disable 假装看不见
const fetchData = () => {
api.getUser(userId);
};
useEffect(() => {
fetchData();
}, []); // 警告: React Hook useEffect has a missing dependency: 'fetchData'
// ✔ 正确解法:将仅在 Effect 内使用的函数移入 Effect 内部
useEffect(() => {
const fetchData = () => {
api.getUser(userId);
};
fetchData();
}, [userId]); // 依赖清晰表达为具体的底层变量 userId
场景 2:计算依赖前一个状态(如计数器或定时器)
问题现象:在定时器中执行 setCount(count + 1),为了避免定时器不断重建,想把依赖项写成 [],导致 ESLint 报警。
// ❌ 错误做法:省略依赖导致闭包中 count 永远是 0,页面卡在 1
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // 报警:缺少依赖 count
// ✔ 正确解法:改用 setState 的函数式更新参数
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 传入更新器函数,无需读取外层的 count
}, 1000);
return () => clearInterval(timer);
}, []); // 真正安全地仅执行一次
场景 3:需要跨多处调用的公共函数
问题现象:函数既需要在按钮点击事件中触发,又需要在 useEffect 中作为初始加载调用。
// ✔ 正确解法:使用 useCallback 稳定函数引用,并将其加入依赖
const loadProfile = useCallback((id) => {
api.fetchProfile(id);
}, []); // 根据该函数真实读取的变量配置依赖
useEffect(() => {
loadProfile(currentId);
}, [loadProfile, currentId]);
场景 4:仅需读取最新数据,无需触发重新执行
问题现象:需要在某个特定事件发生时发送埋点,埋点需要携带当前的最新表单状态,但不希望表单输入时反复触发该副作用。
// ✔ 正确解法:使用 useRef 保存最新状态快照
const formDataRef = useRef(formData);
formDataRef.current = formData; // 保证渲染时同步更新
useEffect(() => {
const socket = connect();
socket.on('disconnect', () => {
sendBeacon(formDataRef.current); // 读取 ref 永远是最新的,且 ref 不需要放入依赖
});
return () => socket.close();
}, []);
六、排查与验证:如何确认规则已生效?
配置完成后,可以通过以下两个步骤验证规则是否在项目中正常运作:
-
编辑器实时提示检查:在 VSCode 等编辑器中确保安装了官方的 ESLint 扩展。编写一个故意遗漏依赖的组件(如在
useEffect内使用count但依赖项传入[])。如果看到代码下方出现黄色或红色波浪线,且悬停提示React Hook useEffect has a missing dependency: 'count',说明编辑器层级已实时生效。 -
命令行全局静态检查:在终端运行 ESLint 扫描命令:
终端应当能够正确打印出相关文件的警告位置与规则名称(npx eslint src/ --ext .js,.jsx,.ts,.tsxreact-hooks/exhaustive-deps)。
七、总结
为 React 项目配置 exhaustive-deps 规则并非单纯为了让语法检查器“闭嘴”,而是为了保障 React 单向数据流在异步与副作用中的确定性。遵循“不要欺骗依赖数组”的原则:
- 凡是在 Effect 内部使用到的响应式值(Props、State 或派生变量),都应如实列在依赖数组中;
- 如果依赖项频繁改变引发重复执行,优先通过将函数内移、使用
useCallback包装、函数式更新或useRef解耦等手段重构代码结构,而非通过禁用来回避问题; - 对自定义 Hook 通过
additionalHooks保持一致的规范约束,确保工程质量的长期健壮。
ReactCompiler1.0之后还要手写useMemo和useCallback吗?
2026-11-12 11:36:08
ReactCompiler1.0怎么开启?自动memo怎么生效?
2026-11-12 08:59:55
JSX的children怎么传?和普通属性有什么区别?
2026-11-12 08:52:16
use(browser())和typeofwindow判断有什么区别?
2026-11-11 14:27:45
React 18 新特性解读:并发模式与自动批处理
2026-11-05 15:19:34








