一、使用 Hook 的两个规则

Hook 是 javascript 函数,但是在使用的时候需要遵循两个规则,而 React 提供了一个 linter 插件来自动执行这些规则。

1、最高级使用 Hook

不要在循环、条件或者嵌套函数中调用 Hook。

使用 Hook 的时候,应当总是在 React 函数的顶层使用。遵循这个规则,能够保证每次组件呈现的时候,都能够以相同的顺序调用 Hook。这也是能够保证 React 在多个 useStateuseEffect 调用之间也能够保留 Hook 的 state 的原因。

2、只在 React Function 中使用 Hooks

不要在常规的 JavaScript 函数中调用 Hook

可以在下面两个地方调用 Hook:

通过遵循这个规则,你可以确保组件中的所有有状态逻辑从其源代码中清晰可见。

二、ESLint 插件

React 发布了一个 eslint-plugin-react-hooks 插件,强制的执行上面的两个 Hook 规则。

如果想要尝试这个插件:

npm install eslint-plugin-react-hooks@next

Eslint config 内容:

// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error"
  }
}

将来,React 会把这个插件包含在 Create React App 之类的工具包中,用以支持 Hook 的使用。

三、规则解释

下面的代码中使用了多个 useStateuseEffect

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

React 是如何知道哪个 state 对应于哪个 uesState 调用的?

答案是因为 React 依赖于调用 Hook 的顺序。

上面的示例有效,是因为 Hook 调用的顺序在每个 render 时都是相同的:

// ------------
// 第一次渲染
// ------------
useState('Mary')           // 1. 初始化 name = 'mary' 的变量
useEffect(persistForm)     // 2. 设置一个 effect 用以保存 form 内容
useState('Poppins')        // 3. 使用 `Poppins` 初始化 surname 变量
useEffect(updateTitle)     // 4. 设置一个 effect 更新 document.title

// -------------
// 第二次渲染
// -------------
useState('Mary')           // 1. 读取 name 变量(忽略参数)
useEffect(persistForm)     // 2. 替换之前设置的 effect ,设置新的 effect 用以保存 form 内容
useState('Poppins')        // 3. 读取 surname 变量(忽略参数)
useEffect(updateTitle)     // 4. 替换之前设置的 effect,设置新的 effect 修改 document.title

只要 Hook 调用的顺序在渲染之间是相同的,React 就可以将一些本地状态与它们中的每一个相关联。但是如果在 if 中放置 Hook 调用(例如, persisForm effect)会发生什么?

// 在 if 中使用了 Hook 打破了第一条规则
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }

name !== '' 在第一次渲染时为 true,因此会运行上面的 Hook,但是在下一次渲染时,用户可能会清除表单,此时条件变成 false。然后会在 render 中跳过这个 Hook,Hook 调用的顺序变得不同:

useState('Mary')           // 1. 读取 name 变量(忽略参数)
// useEffect(persistForm)  // 这个 Hook 被跳过了
useState('Poppins')        // 2 本来顺序应该是 3
useEffect(updateTitle)     // 3  本来顺序应该是 4

React 不知道第二次 useStat Hook调用返回什么。

React 期望此组件中的第二个 Hook 调用对应于 persistForm effect,就像在前一个 render 一样,但它已经不存在了。

而正因为上述情况,在我们跳过的那个 Hook 之后的每个下一个Hook调用也会移动一个位置,导致错误。

这就是必须在我们组件的顶层调用Hooks的原因。如果我们想要有条件地运行一个效果,我们可以把这个条件放在我们的Hook中:

useEffect(function persistForm() {
    // 这里没有打破置顶规则
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

如果使用提供的lint规则,则无需担心此问题