React Hooks 简介
Hook 含义
组件类和函数组件
useState
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| import React, { useState } from 'react';
const Example() {
// 声明了一个 count 的 state 变量,并初始化为 0
// setCount 设置 state 值 count 的方法
const [count, setCount] = useState(0);
// 每次点击按钮,将 count 的值 +1
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</>
)
}
export default Example;
|
语法
1
2
| // 声明一个 state 变量,并同时初始化
const [state, setState] = useState(initialState)
|
- useState 返回一个包含两个元素的数组
- state 变量,指向状态当前值
- setState 更新 state 值的方法
- initialState 状态初始值可以是数字,字符串,数组,对象等
与类中使用 setState
异同
- 相同点
- 在一次渲染周期中调用多次 setState,数据只改变一次
- 不同点
- 类组件中 setState 为合并
- 函数组件中 setState 为替换
useEffect
useEffect 用来执行副作用。可以将 useEffect Hook 看做 componentDidMount, componentDidUpdate, componentWillMount
生命周期函数的组合。
常用在:
- 服务器请求
- 访问元素 dom 元素
- 本地持久化缓存
- 绑定/解绑事件
- 添加订阅
- 设置定时器
- 记录日志
- 等
useEffect 接收一个函数,该函数会在组件渲染完毕后才执行,该函数有要求:要么返回一个能清除副作用的函数,要么就不返回任何内容。
不需要清除的 effect
在 React 更新 DOM 之后,我们想运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。
因为我们在执行完这些操作之后,就可以忽略他们了。
以下为使用 class 和 Hook 都是怎么实现这些副作用的对比。
使用类组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // 在 React 更新 DOM 操作后,立即更新 document 的 title 属性
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() 不能有任何副作用。应该在 react 更新 DOM 后再执行副作用。
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import React, {useState, useEffect} from 'react';
export default function Example() {
const [count, setCount] = useState(0);
useEffect(() => { // 经过渲染或更新, DOM 更新完毕之后,会运行该 effect
document.title = `You click ${count} times`;
});
return (
<div>
<p>You clicked {this.state.count} times.</p>
<button onClick={() => this.setState({ count: this.state.count + 1})}>
click me.
</button>
<div/>
);
}
|
- 第一次渲染之后和每次更新之后都会执行 useEffect。且 React 保证每次运行 effect 的同时,DOM 已经更新完毕。
- useEffect 放在组件内部让我们可以在 effect 中直接访问 state / props。我们不需要特殊的 API 来读取。
- 与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。
需要清除的 effect
有一些副作用是需要清除的,如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!
使用类组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| 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
);
}
componentDidUpdate () {
// ...
}
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';
}
}
|
使用函数组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import React, {useState, useEffect} from 'react';
export default function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// effect 可选清除副作用的函数,返回函数不一定需要命名
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
|
多个 Effect 实现关注点分离
使用 Hook 的其中一个目的是要解决: class 生命周期函数中经常包含不相关逻辑,相关逻辑又分离在几个不同的方法中。
如下为上文中计数器和好友状态指示器逻辑组合在一起的组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
isOnline: null
};
this.handleStautsChange = this.handleStatusChange.bind(this);
}
// 相应的逻辑被分配在三个不同的生命周期函数中
componentDidMount() {
// counter
document.title = `You clicked ${this.state.count} times`;
// friend status
ChatAPI.subscribeToFriendStatus(
this.props.fried.id,
this.handleStatusChange
)
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
}
|
若使用 Hook,和使用多个 state 的 Hook 一样,通过使用多个 effect,将不相关的逻辑分离到不同的 effect 中。
Hook 允许按照代码的用途进行分离,不同于生命周期函数。将按照 effect 声明的顺序依次调用组件中的每一个 effect。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| export default const FriendStatusWithCounter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${this.state.count} times`;
});
const [isOneline, useIsOneline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOneline(status.isOneline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
|
跳过 Effect 进行性能优化
在类组件中,我们可以通过 componentDidUpdate
中 prevProps
和 prevState
的 props / state 的变化比较,判断是否需要执行某些副作用。
1
2
3
4
5
| componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
|
在 Hook 中,可以通过对 useEffect 传递数组作为第二可选参数,通知 React 跳过对 effect 的调用。
1
2
3
4
5
| useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 表示 count 更改时会进行更新
// {... , [5]} 当 count === 5 时,跳过 effect
|
- 若只想运行一次 effect(仅在组件挂载和卸载时执行),可传递一个空数组 [] 作为第二参数。
- 传入一个空数组([]),effect 内部的 props 和 state 会一直拥有其初始值。
- 使用该优化,请确保数组中包含了所有外部作用域中会随时间变化且在 effect 中使用的变量,否则代码会用到先前渲染中的旧变量。
每次更新的时候都要运行 Effect 的原因
自定义 Hook
自定义 Hook 可以将组件逻辑提取到可重用的函数中。
目前为止,在 React 中有两种流行的方式共享组件之间的状态逻辑:
- render props
- 高阶组件
使用 Hook 可以在不增加组件的情况下解决相同的问题。
创建自定义 Hook
当我们想在函数间共享逻辑时,我们可以把它提取到另外一个函数中。由于组件和 Hook 都是函数,所以也同样使用于这种方式。
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
以下是根据上文中 FriendStatus
实例,所提取的自定义 Hook。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import { useState, useEffect } from 'react';
// 名称一定以 use 开头
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOneline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubcribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
export default useFriendStatus;
|
在这个自定义 Hook 中,没有包含任何新内容(逻辑与组件中的完全一致)
使用自定义 Hook
与组件一致,请确保只在自定义 Hook 的顶层无条件地调用其他 Hook。
1
2
3
4
5
6
7
8
| function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return "Loading...";
}
return isOnline ? 'Online' : 'Offline';
}
|
这段代码与之前的工作方式完全一样,我们只是将函数中需要共享的逻辑提取到单独的函数中。
- 自定义 Hook 是一种自然遵循 Hook 设计的约定,不是 React 的特性。
- 自定义 Hook 必须以 “use” 开头
- 不同组件中使用相同的 Hook 不会共享 state。
- 自定义 Hook 是重用状态逻辑的机制(例如设置为订阅并存储当前值),每次使用自定义 Hook 时,其中的所有 state 和副作用完全隔离。
在多个 Hook 之间传递信息
由于 Hook 本身就是函数,因此我们可以在它们之间传递信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // 这是一个聊天消息接收者的选择器,它会显示当前选定的好友是否在线
function ChatRecipientPicker() {
// 当前选择的好友 ID 保存在 recipientID 状态变量中
const [recipientID, setRecipientID] = useState(1);
// 当更新 recipientID 状态变量时,useFriendStatus Hook 会取消订阅之前选中的好友,并订阅新选中的好友状态
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select // <select> 中选择其他好友时更新 recipientID
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{
friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))
}
</select>
</>
)
}
|
我们将当前选择的好友 ID 保存在 recipientID 状态变量中,并会在用户从 <select> 中选择其他好友时更新这个 state。
由于 useState 会提供 recipientID 状态变量的最新值,可以将它作为参数传递给自定义的 useFriendStatus Hook。当我们选择不同的好友并更新 recipientID 状态变量时,useFriendStatus Hook 会取消订阅之前选中的好友,并订阅新选中的好友状态。
Hook 规则
额外的 Hook
参考