React 代码规范:Hooks 的“保镖”与“严父” —— ESLint-plugin-react-hooks 深度解析
各位同学,大家好!
欢迎来到今天的“React 代码规范进阶讲座”。我是你们的老朋友,一个在 React 深渊里摸爬滚打多年,头发日渐稀疏但技术日益硬核的资深工程师。
今天我们要聊的话题,有点“硬核”,有点“枯燥”,甚至有点“强迫症”。但请相信我,如果你不想让你的代码变成一坨无法维护的“屎山”,不想让你的生产环境在半夜三点因为一个不起眼的 Bug 而崩溃,那么,请把你的注意力集中到屏幕上。
我们要聊的,就是 eslint-plugin-react-hooks。
在 React 16.8 之前,我们写组件就像是在玩俄罗斯方块,有明确的规则,有清晰的边界,甚至有点无聊。但自从 Hooks 出现,一切都变了。它给了我们强大的能力,但也给了我们自由。自由,有时候就是一把双刃剑。
当你第一次尝试在 useEffect 里写逻辑,在 if 语句里调用 useState 时,你可能会觉得:“嘿,这代码跑通了啊!React 还真是个魔法师!”
别高兴得太早。魔法师不仅会变魔术,还会在你背后捅刀子。如果你不遵守规则,React 的内部机制——那个基于链表和 Fiber 节点的复杂系统——就会把你刚才那行“看起来很美”的代码,变成一场逻辑灾难。
今天,我们就来聊聊如何通过 eslint-plugin-react-hooks 这个“严父”,来强制规范你的 Hooks 使用习惯。
第一部分:Hooks 的“灵魂”法则
在深入代码之前,我们先得明白一个核心概念:为什么 Hooks 不能随便写?
React 的核心思想之一是“可预测性”。当你写一个函数式组件时,React 需要在你的组件被渲染时,按顺序执行你的代码。Hooks(useState, useEffect, useContext 等)本质上就是 React 内部维护的一个状态链表。
想象一下,你走进一家餐馆点餐。
- 规则 1(顶层调用): 你不能说“我要一杯水”,然后因为外面下雨了,就改成“我要一杯可乐”。你必须坐在座位上,按顺序点完所有的菜:先点前菜,再点主菜,最后点饮料。每一道菜都有固定的编号。
- 规则 2(只能在 React 函数中调用): 你不能在服务员递给你菜单的时候,或者在你去洗手间的时候,突然掏出一个锅铲开始炒菜。你必须坐在餐桌前,用指定的餐具(React 提供的钩子)来操作。
如果你违反了规则 1,比如在 if 语句里调用 useState,React 就会崩溃,因为它不知道你的链表该指向哪里。如果你违反了规则 2,比如在 handleClick 事件处理函数里调用 useEffect,每次点击按钮,React 都会以为你开启了一个新的“生命周期”,导致逻辑混乱。
而 eslint-plugin-react-hooks,就是那个站在门口的保安,它手里拿着一张黑名单,一旦发现你试图在错误的地方调用 Hooks,就会立刻把你拦下来。
第二部分:安装与配置 —— 像系安全带一样安装插件
好,废话不多说,我们来实操。怎么安装这个“保安”呢?
npm install eslint-plugin-react-hooks --save-dev
安装完之后,你需要告诉 ESLint 来使用它。这通常在 .eslintrc.js 或 .eslintrc.json 文件中。
module.exports = {
// ... 其他配置
plugins: [
// 别忘了引入 react 和 react-hooks
'react',
'react-hooks'
],
rules: {
// 开启 Hooks 规则
'react-hooks/rules-of-hooks': 'error',
// 开启依赖项检查
'react-hooks/exhaustive-deps': 'error'
}
};
注意了,'react-hooks/rules-of-hooks' 是强制执行的,它是底线。一旦违反,代码直接报错,无法运行。而 'react-hooks/exhaustive-deps' 则是进阶检查,它会帮你发现那些隐形的 Bug。
第三部分:规则详解与代码示例 —— 避坑指南
接下来,我们进入最精彩的环节。我会列举一些违反规则的黑历史代码,然后告诉你为什么它们是错的,以及如何用 ESLint 提示来纠正它们。
场景一:在循环、条件语句或嵌套函数中调用 Hooks
这是新手最容易犯的错误。你以为你在写普通的 JS 代码,但在 React 眼里,这是在搞破坏。
❌ 错误示范:在 if 语句中调用 useState
import React, { useState, useEffect } from 'react';
function BadComponent({ isLoggedIn }) {
// 错误!React 期望每次渲染都调用相同数量的 Hooks。
// 这里,如果 isLoggedIn 为 false,count 就不会被定义。
if (!isLoggedIn) {
return <div>请先登录</div>;
}
const [count, setCount] = useState(0); // 危险!
const [name, setName] = useState('React Dev');
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
为什么会报错?
ESLint 会直接给你一记响亮的耳光。因为 React 的内部机制是基于调用顺序的。第一次渲染时,如果 isLoggedIn 是 false,React 执行到 if 就停了,它根本没机会执行 useState。状态链表是空的。
第二次渲染,如果 isLoggedIn 变成了 true,React 发现了 useState,它会认为这是状态 0。但是,由于上一次渲染没有执行 useState,React 就会困惑:状态 1 去哪了?状态 2 去哪了?
结果就是:你的状态索引错位了。 你以为你在操作 count,实际上你可能在操作 name。这就是传说中的“幽灵 Bug”。
✅ 正确示范:
function GoodComponent({ isLoggedIn }) {
// 哪怕是 false,我也得把 Hooks 声明出来!
const [count, setCount] = useState(0);
const [name, setName] = useState('React Dev');
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
if (!isLoggedIn) {
return <div>请先登录</div>;
}
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
你看,虽然多写了一行,但 React 的内心是平静的。这就是规范的力量。
❌ 错误示范:在嵌套函数中调用 useEffect
function BadComponent2() {
const [data, setData] = useState([]);
// 错误!每次渲染都创建一个新的函数,每次渲染都执行 Effect。
const fetchData = () => {
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []); // 这里的依赖数组其实没起作用,因为每次 render 都变
};
fetchData();
return <div>{JSON.stringify(data)}</div>;
}
为什么会报错?
ESLint 会提示你:React Hook useEffect is called in a function. React Hooks must be called in the body of a function component.
这里的问题在于,你试图把副作用逻辑封装在一个函数里,但这破坏了 React 的渲染周期。每次渲染,fetchData 都会被重新定义,导致 useEffect 被重新挂载,造成无限循环或资源泄漏。
✅ 正确示范:
function GoodComponent2() {
const [data, setData] = useState([]);
useEffect(() => {
// 直接把逻辑写在顶层,不要套娃!
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []); // 依赖数组在这里才真正有效
return <div>{JSON.stringify(data)}</div>;
}
场景二:在 React 函数之外调用 Hooks
这通常发生在全局变量或者工具函数中。
❌ 错误示范:
// utils.js
import { useState } from 'react';
// 错误!你在模块顶层调用了 Hooks。
// 这意味着每次引入这个模块,都会创建新的状态,但组件不会重新渲染。
export function useGlobalState() {
const [state, setState] = useState('global');
return [state, setState];
}
为什么会报错?
React 是基于组件实例的。Hooks 属于某个特定的组件实例。如果你在组件外部调用,React 根本不知道该把这个状态绑定到哪个组件上。
✅ 正确示范:
Hooks 只能在 React 组件函数(包括自定义 Hooks)的顶层调用。
// utils.js
import { useState, useEffect } from 'react';
// 正确:自定义 Hooks 必须以 use 开头,并且只能在 React 组件内部调用
export function useWindowSize() {
const [size, setSize] = useState([window.innerWidth, window.innerHeight]);
useEffect(() => {
const handleResize = () => setSize([window.innerWidth, window.innerHeight]);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
场景三:依赖项地狱 —— exhaustive-deps 规则
这是 ESLint-plugin-react-hooks 最强大,也最容易让人抓狂的功能。
问题背景:
useEffect 的第二个参数是一个依赖数组。如果你在 useEffect 里用到了组件里的变量(比如 props.user 或 state.count),你必须把这个变量放进依赖数组里。否则,如果这个变量变了,你的 useEffect 不会重新执行,导致逻辑滞后。
❌ 错误示范:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 错误!userId 没有在依赖数组里。
// 如果 userId 变了,这个 fetch 不会执行,页面显示的还是旧数据!
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, []);
if (!user) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
ESLint 会提示你:React Hook useEffect has a missing dependency: 'userId'. Either include it or remove the dependency array.
✅ 正确示范(修复版):
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]); // 修复!把 userId 加进去
if (!user) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
进阶问题:无限循环
有时候,你加了依赖项,代码虽然不报错了,但组件开始疯狂渲染,CPU 占用飙升。
❌ 错误示范:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// 错误!setCount 是一个函数,它本身每次渲染都会生成一个新的引用。
// 所以它每次都在依赖数组里,导致 useEffect 每次都重新运行。
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timer);
}, [setCount]); // 致命!这会导致无限循环
return <div>{count}</div>;
}
ESLint 会提示你:React Hook useEffect has a missing dependency: 'setCount'. Either include it or remove the dependency array.
✅ 正确示范(消除副作用):
我们需要把“副作用”和“状态更新”分开。状态更新不应该触发副作用,除非副作用本身是为了更新状态(这通常需要用 useLayoutEffect 或特定的模式,这里暂不展开)。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // 修复!setCount 是由 React 提供的稳定引用,不需要依赖它
return <div>{count}</div>;
}
高级技巧:使用 useCallback 或 useRef
有时候,你真的需要在依赖数组里放一个函数,但那个函数每次渲染都不一样。这时候,useCallback 就派上用场了。
✅ 正确示范(优化版):
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 使用 useCallback 包装函数,确保它在依赖项不变的情况下引用不变
const fetchUser = useCallback(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
useEffect(() => {
fetchUser();
}, [fetchUser]); // 依赖 fetchUser,而不是直接依赖 userId,避免闭包陷阱
if (loading) return <div>加载中...</div>;
return <div>{user.name}</div>;
}
第四部分:自定义 Hooks —— 规范的延伸
既然我们有了 eslint-plugin-react-hooks,我们就可以放心大胆地创建自己的 Hooks 来封装复杂的逻辑。自定义 Hooks 本质上就是把组件拆分成更小的逻辑单元。
场景:表单验证
假设我们要写一个通用的表单验证逻辑。
❌ 错误示范(没有封装):
function BadLoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
useEffect(() => {
const newErrors = {};
if (!email.includes('@')) newErrors.email = 'Invalid email';
if (password.length < 6) newErrors.password = 'Password too short';
setErrors(newErrors);
}, [email, password]);
// ... 渲染逻辑
}
如果你有 10 个表单,你就要复制粘贴 10 次这段逻辑。而且,这段逻辑和渲染逻辑混在一起,很难维护。
✅ 正确示范(封装成自定义 Hook):
// useValidation.js
import { useState, useEffect } from 'react';
export function useValidation(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isTouched, setIsTouched] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({ ...values, [name]: value });
};
const handleBlur = () => {
setIsTouched(true);
};
useEffect(() => {
if (isTouched) {
setErrors(validate(values));
}
}, [values, isTouched, validate]);
return {
values,
errors,
isTouched,
handleChange,
handleBlur,
reset: () => {
setValues(initialValues);
setErrors({});
setIsTouched(false);
}
};
}
现在,我们在组件里使用它:
import { useValidation } from './useValidation';
const validate = (values) => {
const errors = {};
if (!values.email) errors.email = 'Required';
else if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(values.email)) errors.email = 'Invalid email address';
return errors;
};
function GoodLoginForm() {
const {
values,
errors,
handleChange,
handleBlur,
reset
} = useValidation({ email: '', password: '' }, validate);
// 使用起来非常清爽,而且 ESLint 会自动检查我们在这个 Hook 内部是否遵守了 Hooks 规则
return (
<form>
<input
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span>{errors.email}</span>}
{/* ... */}
</form>
);
}
通过自定义 Hooks,我们不仅把代码变得整洁,还把“规则”封装在了 Hook 的内部。外部组件只需要遵循简单的调用规范,而复杂的逻辑实现细节则被隐藏在自定义 Hook 的实现中。
第五部分:高级配置与最佳实践
作为资深工程师,我们不能只满足于让代码跑起来,我们还要让它跑得优雅。
1. TypeScript 集成
如果你的项目是 TS 写的,eslint-plugin-react-hooks 配合 TypeScript 类型检查会发挥出更强大的威力。它能帮你检查依赖项的类型,防止因为类型错误导致的运行时崩溃。
确保你的 tsconfig.json 和 eslint 配置正确关联。
2. 忽略特定规则(慎用)
有时候,我们真的需要做一些“不合规”的操作,比如在 useEffect 里动态注入脚本。这时候,不要直接关掉规则,而是使用 ESLint 的 // eslint-disable-next-line 注释,并解释原因。
useEffect(() => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const script = document.createElement('script');
script.src = 'https://example.com/script.js';
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, []); // 依赖数组为空,因为我们不想在每次渲染时都重新加载脚本
3. no-unstable-nested-components 规则
这个规则通常配合 react/jsx-filename-extension 一起使用。它强制要求组件必须定义在文件顶层,不能嵌套在函数内部。这有助于代码的可读性和 tree-shaking(树摇优化)。
// ❌ 错误:嵌套组件
function BadComponent() {
const Child = () => <div>Child</div>;
return <Child />;
}
// ✅ 正确:顶层组件
const Child = () => <div>Child</div>;
function GoodComponent() {
return <Child />;
}
第六部分:总结与展望
好了,同学们,今天的讲座接近尾声。
我们今天聊了什么?
我们聊了 eslint-plugin-react-hooks 这个像严父一样的工具。我们明白了为什么 Hooks 不能在 if 里调用,为什么依赖数组不能漏项,以及如何通过自定义 Hooks 来封装逻辑。
你可能觉得,这些规则很繁琐,很限制自由。但请记住,自由不是想干什么就干什么,而是有能力控制你不该干什么。
在 React 的世界里,规则不是为了束缚你的手脚,而是为了确保你的代码在复杂的渲染周期中依然保持稳定。当你把 eslint-plugin-react-hooks 配置好,并严格遵守它时,你会发现,那些曾经让你抓耳挠腮的 Bug,那些因为状态错位导致的奇怪行为,都会烟消云散。
React 的核心在于“声明式”,而 Hooks 的核心在于“确定性”。eslint-plugin-react-hooks 就是那个守护“确定性”的守门人。
最后,给大家留个作业:
去检查你的项目,看看有多少地方违反了 rules-of-hooks 规则。如果有的话,别犹豫,立刻修复它。这就像打扫你的房间,一开始会觉得麻烦,但当你下次打开代码文件时,那种整洁和清爽,会让你觉得所有的努力都是值得的。
记住,优秀的代码,是写给别人看的,顺便给机器运行。 而遵守 Hooks 规则,就是让代码变得优雅的第一步。
现在,去写代码吧!让你的 Hooks 们,在规范的轨道上飞驰!