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用来保证这两点规则。你可以这样把插件添加到你的项目中:
npm install eslint-plugin-react-hooks --save-dev
// Your ESLint configuration
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
}
}
未来我们打算将此插件打包到Create React App和一些类似的工具中去。
现在,你可以跳到下一讲看看怎样自定义自己的Hooks。或者在这一讲,我们继续看看这些规则背后原因的解释。
解释
正如之前所学,我们可以在一个组件中使用多个State或者Effect Hooks:
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和那个setState相对应?答案就是React按照Hook的调用次序。我们的组件之所以正常工作是因为每次渲染Hook调用的次序是一样的:
// ------------
// First render
// ------------
useState('Mary') // 1. 初始化状态值 'Mary'
useEffect(persistForm) // 2. 添加一个副作用 保存表单
useState('Poppins') // 3. 初始化状态值'Poppins'
useEffect(updateTitle) // 4. 添加一个副作用更新 title
// -------------
// Second render
// -------------
useState('Mary') // 1. 读取name的状态值 (参数忽略了)
useEffect(persistForm) // 2. 代替副作用来更新表单
useState('Poppins') // 3. 读取 surname 状态值 (参数忽略)
useEffect(updateTitle) // 4. 代替副作用来更新 title
// ...
只要Hooks的调用按照相同的顺序,React就能把它们每一个和本地状态联系起来。但是要是我们把Hook放在条件中会怎样?
// 🔴 我们正在打破第一条规则,我们居然把hook放到条件里面了
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
name !== ‘’ 条件在第一次渲染的时候是true,所以该hook执行。但是,在下一次渲染用户可能清除了表单,就把条件的结果变成false了。现在我们在渲染期间跳过了这个hook,hook的调用顺序因此变得不同:
useState('Mary') // 1. 读取name这个状态的值 (参数不计)
// useEffect(persistForm) // 🔴 这个 Hook 跳过!
useState('Poppins') // 🔴 实际是2 (但是应该是 3). 读取surname状态值失败
useEffect(updateTitle) // 🔴 实际是3 (但是应该是 4). 执行副作用失败
React不知道在第二个useState调用的时候返回什么。React预期组件中的第二个Hook是与persistForm这个effect副作用一致,正如在第一次渲染的时候那样,但是却不是。从那时起,我们跳过的Hook之后的每一个Hook都会提前执行,这就导致bug了。
这就是为什么Hook要在组件的顶层调用。如果我们想有条件执行effect副作用,我们可以把条件放在Hook内部:
useEffect(function persistForm() {
// 👍 We're not breaking the first rule anymore
if (name !== '') {
localStorage.setItem('formData', name);
}
});
注意:如果使用了提供的 lint 插件,就无需担心此问题。 不过你现在知道了为什么 Hook 会这样工作,也知道了这个规则是为了避免什么问题。
下一步
最终,我们准备好了学习编写自定义Hooks了!自定义Hook能让你将React的Hook在你自己的抽象逻辑中结合,并且在不同的组件间复用这些有状态的逻辑。