Hooks 规则

Hooks 是一项新功能提案,可让您在不编写类的情况下使用 state(状态) 和其他 React 功能。它们目前处于 React v16.7.0-alpha 中,并在 一个开放RFC 中进行讨论。

Hooks 是 JavaScript 函数,但在使用它们时需要遵循两个规则。 我们提供了一个 linter 插件 来自动执行这些规则:

只在顶层调用Hook

**不要在循环,条件或嵌套函数中调用 Hook **。 相反,总是在 React 函数的顶层使用 Hooks。 通过遵循此规则,您可以确保每次组件渲染时都以相同的顺序调用 Hook 。 这就是允许 React 在多个 useStateuseEffect 调用之间能正确保留 Hook 状态的原因。 (如果你很好奇,我们将在 下面 深入解释。)

只在 React Functions 调用 Hooks

不要在常规 JavaScript 函数中调用 Hook 。 相反,你可以:

  • ✅ 在 React 函数式组件中调用 Hooks 。
  • ✅ 从自定义 Hooks 调用 Hooks (我们将在下一页中学习 自定义 Hooks )。

通过遵循此规则,您可以确保组件中的所有 stateful (有状态)逻辑在其源代码中清晰可见。

ESLint 插件

我们发布了一个名为 eslint-plugin-react-hooks 的ESLint插件,它强制执行这两个规则。如果您想尝试,可以将此插件添加到项目中:

npm install eslint-plugin-react-hooks@next
// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error"
  }
}

将来,我们打算默认将此插件包含在 Create React App 和类似的工具箱中。

**您可以跳到下一页,解释如何编写 自己的 Hooks **(愚人码头注:自定义 Hooks)。 在此页面上,我们将继续解释这些规则背后的原因。

解释说明

如前所述,我们可以在单个组件中使用多个 State 或 Effect Hook:

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(状态) 对应于哪个 useState 调用呢?答案是 React 依赖于调用 Hooks 的顺序。我们的示例之所以有效,是因为每次渲染时 Hook 调用的顺序都是相同的:

// ------------
// 第一次渲染
// ------------
useState('Mary')           // 1. 用'Mary'初始化名称状态变量
useEffect(persistForm)     // 2. 添加一个 effect 用于持久化form
useState('Poppins')        // 3. 使用 'Poppins' 初始化 surname 状态变量
useEffect(updateTitle)     // 4. 添加一个 effect 用于更新 title

// -------------
// 第二次渲染
// -------------
useState('Mary')           // 1. 读取 name  状态变量(忽略参数)
useEffect(persistForm)     // 2. 替换 effect 以持久化 form
useState('Poppins')        // 3. 读取 surname 状态变量(忽略参数)
useEffect(updateTitle)     // 4. 替换 effect 用于更新 title

// ...

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

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

name !== '' 条件在第一次渲染时为 true ,因此我们运行此 Hook 。 但是,在下一次渲染时,用户可能会清除form,使条件置为 false 。 现在我们在渲染过程中跳过此 Hook ,Hook 调用的顺序变得不同:

useState('Mary')           // 1. 读取 name  状态变量(忽略参数)
// useEffect(persistForm)  // 🔴 这个Hook被跳过了
useState('Poppins')        // 🔴 2 (但是之前是 3). 读取 surname 状态变量失败
useEffect(updateTitle)     // 🔴 3 (但是之前是 4). 替换 effect 失败 

React 不知道第二次 useState Hook 调用返回什么。React 期望这个组件中的第二个 Hook 调用对应于 persistForm effect,就像之前的渲染一样,但现在已经不存在了。从那时起,在我们跳过的那个 Hook 调用之后的每一个 Hook 调用也会移动一个,从而导致 bug。

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

  useEffect(function persistForm() {
    // 👍 我们不再违反第一条规则了
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

请注意,如果使用 我们提供的lint规则 的话,就不需要担心这个问题。但是现在您也知道了 为什么 Hooks 以这种方式工作,以及规则阻止了哪些问题。

下一步

最后,我们已经准备好了解如何 编写自己的Hooks ! 自定义 Hooks 允许您将 React 提供的 Hook 组合到您自己的抽象中,并重用不同组件之间的常见 stateful(有状态) 逻辑。