「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」
一直想学(懒得学)React Hook,今天认真看完了官方文档,不得不说Hook真香,为维护复杂数据状态的类组件提供了解决方案。
Hook,顾名思义就是钩子函数,也就是说本质上Hook是函数,具体点就是Hook是React 16.8版本内部新增的应用于函数组件对外的函数。
一般情况下,useState和useEffect这两个Hook以及自定义Hook可以满足大多数应用场景。
一般useState称作State Hook,useEffect称作Effect Hook。
Hook 简介
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hook没破坏性改动:
- 完全可选的
- 100%向后兼容
- 现在可用
React官方没有计划移除class,无需重构现有类组件代码,如果你目前在编写类组件的React应用,后续可以尝试在项目使用Hook编写,渐进使用它们,Class和Hook互不影响。
Hook 解决了什么问题?
- 无需修改组件结构的情况下复用状态逻辑。
- 将组件中相互关联的部分拆分更小的函数,使其更容易理解
- 摆脱了新手理解class以及this的困难
State Hook
State Hook就是useState钩子函数,接下来举例对比Class和Hook并分析useState到底做了什么事。
「Class示例」
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
复制代码
「等价的Hook示例」
import React, { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
复制代码
useState接收唯一的参数(数字、字符串...也可以是对象),并返回两个值存放在数组中,我这里用x,y表示,因此其返回值为[x, y],其中参数表示state初始值,x表示更新后的state,y表示更新state的函数,组件首次挂载时,state等于useState的传参。
Hook示例中代码 const [count, setCount] = useState(0) 表示我们声明了一个叫count的state变量和一个更新state变量的方法(功能等同于this.setState()),使用ES6数组解构的方式被桉顺序赋值,其中count被赋值x,setCount被赋值y。
在函数作用域中,可以直接使用变量,不带this,因此在Hook示例中,直接使用state变量和setCount()方法。
Effect Hook
Effect Hook就是useEffect钩子函数。
useEffect等同于Raact的生命周期componentDidMount()、componentDidUpdate()和compnentWillUnmount()三个函数的组合。
可以在useEffect中进行数据请求、设置订阅、操作DOM等操作。
接下来举例对比Class和Hook并分析useEffect。
分两种,无需清除的Effect和需要清除的Effect.
无需清除的Effect
「Class示例」
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
复制代码
「等价的Hook示例」
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
复制代码
此处的的useEffect等同于componentDidMount()、componentDidUpdate()的组合。
需要清除的Effect
举个获取好友状态的例子对比展示
「Class示例」
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
复制代码
「等价的Hook示例」
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
复制代码
需要清除的Effect,指的是useEffect会返回一个函数,代替componentWillUnmount()函数执行解除订阅。 此处的的useEffect等同于componentDidMount()、componentDidUpdate()和componentWillUnmount()的组合。
Hook规则
使用它时需要遵循两条规则:
-
只在最顶层使用Hook
-
只在React函数中调用Hook
- 在React函数组件中调用Hook
- 在自定义Hook中调用其他Hook
对于第一条官方给的规则,我理解的是只在最顶层函数作用域使用Hook,官方说不要在循环,条件或嵌套函数中调用 Hook,主要是确保Hook每次渲染都按照同样的顺序被调用,这让React能够在多次的useState和useEffect调用之间保持Hook状态的正确。
举个例子:
正常遵守规则情况下,多个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;
});
// ...
// ------------
// 首次渲染
// ------------
useState('Mary') // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm) // 2. 添加 effect 以保存 form 操作
useState('Poppins') // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle) // 4. 添加 effect 以更新标题
// -------------
// 二次渲染
// -------------
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm) // 2. 替换保存 form 的 effect
useState('Poppins') // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle) // 4. 替换更新标题的 effect
// ...
}
复制代码
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。 但如果我们将一个 Hook (例如 persistForm effect) 调用放到一个条件语句中会发生什么呢?
// 在条件语句中使用 Hook 违反第一条规则
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
复制代码
在第一次渲染中 name !== '' 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm) // 此 Hook 被忽略!
useState('Poppins') // 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle) // 3 (之前为 4)。替换更新标题的 effect 失败
复制代码
React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。
**「是为什么 Hook 需要在我们组件的最顶层调用」**果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:
useEffect(function persistForm() {
// 👍 将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});
复制代码
注:官方为我们提供了lint插件eslint-plugin-react-hooks,使用了就无需担心此问题!
自定义Hook
自定义Hook更像是一种约定,必须以use开头来命名函数。
自定义Hook就是把两个函数组件之间共用的逻辑抽离出来。
举个例子:
组件一:用于显示好友的在线状态
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
复制代码
组件二:聊天应用中有一个联系人列表,当用户在线时需要把名字设置为绿色
import React, { useState, useEffect } from 'react';
function FriendListItem(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
复制代码
提取自定义Hook,返回好友在线状态Boolean值
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
复制代码
分别在组件一和组件二中使用自定义Hook
组件一使用自定义Hook:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
复制代码
组件二使用自定义Hook
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
复制代码
代码逻辑清晰度显而易见。
Hook API 索引
一般情况下useState和useEffect就可以满足我们的需求,此处贴下所有Hook,有业务场景可以传送到官网学习下。
-
基础Hook
-
额外Hook
总结
以上例子和部分术语参考React官方文档,适合React新手学习参考,本人也是React新手,后期会计划边学边输出React基础系列,与大家共同成长。