React 代码规范:结合 ESLint-plugin-react-hooks 强制执行 Hooks 编写规则的工程规范

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 的内部机制是基于调用顺序的。第一次渲染时,如果 isLoggedInfalse,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.userstate.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>;
}

高级技巧:使用 useCallbackuseRef

有时候,你真的需要在依赖数组里放一个函数,但那个函数每次渲染都不一样。这时候,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.jsoneslint 配置正确关联。

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 们,在规范的轨道上飞驰!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注