原文:Rules of Hooks

Hooks是React 16.8的新特性。这些特性让你不必专门写class组件,来使用state和其他React特性。

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

只能在顶层调用Hooks

不要在循环,条件,或者嵌套的函数中调用Hooks。反之,必须在React函数的顶层使用Hooks。遵循这条规则,你就能保证在每次组件渲染中Hooks以相同的顺序调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。(如果你对此感到好奇,我们在后文中会有更深入的解释。)

只能再React函数中调用Hooks

别在普通的JavaScript函数中调用Hooks。反之,你可以:

  • 在React函数组件中使用;
  • 在自定义Hooks中使用(下一讲我们就学习);

遵照这个规则,确保在组件中有状态的逻辑从源代码中就清晰可见。

ESLint 插件

我们发布了ESLint:eslint-plugin-react-hooks用来保证这两点规则。你可以这样把插件添加到你的项目中:

  1. npm install eslint-plugin-react-hooks --save-dev
  1. // Your ESLint configuration
  2. {
  3. "plugins": [
  4. // ...
  5. "react-hooks"
  6. ],
  7. "rules": {
  8. // ...
  9. "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
  10. "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  11. }
  12. }

未来我们打算将此插件打包到Create React App和一些类似的工具中去。

现在,你可以跳到下一讲看看怎样自定义自己的Hooks。或者在这一讲,我们继续看看这些规则背后原因的解释。

解释

正如之前所学,我们可以在一个组件中使用多个State或者Effect Hooks:

  1. function Form() {
  2. // 1. Use the name state variable
  3. const [name, setName] = useState('Mary');
  4. // 2. Use an effect for persisting the form
  5. useEffect(function persistForm() {
  6. localStorage.setItem('formData', name);
  7. });
  8. // 3. Use the surname state variable
  9. const [surname, setSurname] = useState('Poppins');
  10. // 4. Use an effect for updating the title
  11. useEffect(function updateTitle() {
  12. document.title = name + ' ' + surname;
  13. });
  14. // ...
  15. }

那么React怎么知道那个state和那个setState相对应?答案就是React按照Hook的调用次序。我们的组件之所以正常工作是因为每次渲染Hook调用的次序是一样的:

  1. // ------------
  2. // First render
  3. // ------------
  4. useState('Mary') // 1. 初始化状态值 'Mary'
  5. useEffect(persistForm) // 2. 添加一个副作用 保存表单
  6. useState('Poppins') // 3. 初始化状态值'Poppins'
  7. useEffect(updateTitle) // 4. 添加一个副作用更新 title
  8. // -------------
  9. // Second render
  10. // -------------
  11. useState('Mary') // 1. 读取name的状态值 (参数忽略了)
  12. useEffect(persistForm) // 2. 代替副作用来更新表单
  13. useState('Poppins') // 3. 读取 surname 状态值 (参数忽略)
  14. useEffect(updateTitle) // 4. 代替副作用来更新 title
  15. // ...

只要Hooks的调用按照相同的顺序,React就能把它们每一个和本地状态联系起来。但是要是我们把Hook放在条件中会怎样?

  1. // 🔴 我们正在打破第一条规则,我们居然把hook放到条件里面了
  2. if (name !== '') {
  3. useEffect(function persistForm() {
  4. localStorage.setItem('formData', name);
  5. });
  6. }

name !== ‘’ 条件在第一次渲染的时候是true,所以该hook执行。但是,在下一次渲染用户可能清除了表单,就把条件的结果变成false了。现在我们在渲染期间跳过了这个hook,hook的调用顺序因此变得不同:

  1. useState('Mary') // 1. 读取name这个状态的值 (参数不计)
  2. // useEffect(persistForm) // 🔴 这个 Hook 跳过!
  3. useState('Poppins') // 🔴 实际是2 (但是应该是 3). 读取surname状态值失败
  4. useEffect(updateTitle) // 🔴 实际是3 (但是应该是 4). 执行副作用失败

React不知道在第二个useState调用的时候返回什么。React预期组件中的第二个Hook是与persistForm这个effect副作用一致,正如在第一次渲染的时候那样,但是却不是。从那时起,我们跳过的Hook之后的每一个Hook都会提前执行,这就导致bug了。
这就是为什么Hook要在组件的顶层调用。如果我们想有条件执行effect副作用,我们可以把条件放在Hook内部:

  1. useEffect(function persistForm() {
  2. // 👍 We're not breaking the first rule anymore
  3. if (name !== '') {
  4. localStorage.setItem('formData', name);
  5. }
  6. });

注意:如果使用了提供的 lint 插件,就无需担心此问题。 不过你现在知道了为什么 Hook 会这样工作,也知道了这个规则是为了避免什么问题。

下一步

最终,我们准备好了学习编写自定义Hooks了!自定义Hook能让你将React的Hook在你自己的抽象逻辑中结合,并且在不同的组件间复用这些有状态的逻辑。