blog: 更新React hook笔记
This commit is contained in:
parent
9748c8df8c
commit
7a662a9288
|
|
@ -1,54 +0,0 @@
|
||||||
---
|
|
||||||
title: Hook
|
|
||||||
date: 2021-03-27 17:48:03
|
|
||||||
permalink: /pages/4c13b9/
|
|
||||||
categories:
|
|
||||||
- 《React》笔记
|
|
||||||
- Hook
|
|
||||||
tags:
|
|
||||||
- React
|
|
||||||
---
|
|
||||||
# 01. Hook
|
|
||||||
|
|
||||||
Hook可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## useState使用方式
|
|
||||||
|
|
||||||
**模式:**
|
|
||||||
|
|
||||||
```js
|
|
||||||
const [<取值>, <设值>] = useState(<初始值>)
|
|
||||||
```
|
|
||||||
|
|
||||||
上面的三个值均可自定义名称,分别是:
|
|
||||||
|
|
||||||
- state取值变量名
|
|
||||||
- state设值方法名
|
|
||||||
- 初始值
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**例子:**
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
function Example() {
|
|
||||||
// 声明一个新的叫做 “count” 的 state 变量
|
|
||||||
const [count, setCount] = useState(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>You clicked {count} times</p>
|
|
||||||
<button onClick={() => setCount(count + 1)}>
|
|
||||||
Click me
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
---
|
||||||
|
title: Hook概述
|
||||||
|
date: 2021-03-31 11:30:03
|
||||||
|
permalink: /pages/4c13b9/
|
||||||
|
categories:
|
||||||
|
- 《React》笔记
|
||||||
|
- Hook
|
||||||
|
tags:
|
||||||
|
- React
|
||||||
|
---
|
||||||
|
# 01. Hook概述
|
||||||
|
|
||||||
|
Hook可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 什么是 Hook?
|
||||||
|
|
||||||
|
Hook 是一个特殊的函数,可以让你在函数组件里“钩入” React **state** 及 **生命周期** 等特性的函数。
|
||||||
|
|
||||||
|
> 1. Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。
|
||||||
|
>
|
||||||
|
> 2. React 内置了一些像 `useState` 这样的 Hook。你也可以创建你自己的 Hook 来复用不同组件之间的状态逻辑。我们会先介绍这些内置的 Hook。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## useState(状态钩子)
|
||||||
|
|
||||||
|
**模式:**
|
||||||
|
|
||||||
|
```js
|
||||||
|
const [<取值>, <设值>] = useState(<初始值>)
|
||||||
|
```
|
||||||
|
|
||||||
|
上面的三个值均可自定义名称,分别是:
|
||||||
|
|
||||||
|
- state取值变量名
|
||||||
|
- state设值方法名
|
||||||
|
- 初始值
|
||||||
|
- 初始值可以是数字、字符串、对象、数组等
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**例子:**
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
function Example() {
|
||||||
|
// 声明一个新的叫做 “count” 的 state 变量
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>You clicked {count} times</p>
|
||||||
|
<button onClick={() => setCount(count + 1)}>
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 声明多个 state 变量
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
function ExampleWithManyStates() {
|
||||||
|
// 声明多个 state 变量!
|
||||||
|
const [age, setAge] = useState(42);
|
||||||
|
const [fruit, setFruit] = useState('banana');
|
||||||
|
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Effect Hook (副作用钩子)
|
||||||
|
|
||||||
|
你之前可能已经在 React 组件中执行过**数据获取、订阅 或 者手动修改 DOM**。我们统一把这些操作称为“**副作用**”,或者简称为“作用”。
|
||||||
|
|
||||||
|
### useEffect(生命周期钩子)
|
||||||
|
|
||||||
|
`useEffect` 就是一个 Effect Hook,**给函数组件增加了操作副作用的能力**。它跟 class 组件中的 `componentDidMount`、`componentDidUpdate` 和 `componentWillUnmount` 具有相同的用途,只不过被合并成了一个 API。(我们会在[使用 Effect Hook](https://zh-hans.reactjs.org/docs/hooks-effect.html) 里展示对比 `useEffect` 和这些方法的例子。)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
例如,下面这个组件在 React 更新 DOM 后会设置一个页面标题:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
function Example() {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
// 相当于 componentDidMount 和 componentDidUpdate:
|
||||||
|
useEffect(() => {
|
||||||
|
// 第一次渲染DOM以及更新DOM后执行此函数
|
||||||
|
// 使用浏览器的 API 更新页面标题
|
||||||
|
document.title = `You clicked ${count} times`;
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>You clicked {count} times</p>
|
||||||
|
<button onClick={() => setCount(count + 1)}>
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当你调用 `useEffect` 时,就是在告诉 React **在完成对 DOM 的更改后运行你的“副作用”函数**。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— **包括**第一次渲染的时候。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### “清除”副作用
|
||||||
|
|
||||||
|
副作用函数还可以通过返回一个函数来指定如何“清除”副作用。例如,在下面的组件中使用副作用函数来订阅好友的在线状态,并通过取消订阅来进行清除操作:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
function FriendStatus(props) {
|
||||||
|
const [isOnline, setIsOnline] = useState(null);
|
||||||
|
|
||||||
|
function handleStatusChange(status) {
|
||||||
|
setIsOnline(status.isOnline);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => { // 组件挂载完成和更新完成后执行
|
||||||
|
// 创建订阅
|
||||||
|
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
|
||||||
|
return () => { // 组件销毁时执行
|
||||||
|
// 取消订阅
|
||||||
|
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (isOnline === null) {
|
||||||
|
return 'Loading...';
|
||||||
|
}
|
||||||
|
return isOnline ? 'Online' : 'Offline';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在这个示例中,React 会在组件销毁时取消对 `ChatAPI` 的订阅,然后在后续渲染时重新执行副作用函数。(如果传给 `ChatAPI` 的 `props.friend.id` 没有变化,你也可以[告诉 React 跳过重新订阅](https://zh-hans.reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects)。)
|
||||||
|
|
||||||
|
跟 `useState` 一样,你可以在组件中多次使用 `useEffect` :
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
function FriendStatusWithCounter(props) {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = `You clicked ${count} times`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isOnline, setIsOnline] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
|
||||||
|
return () => {
|
||||||
|
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleStatusChange(status) {
|
||||||
|
setIsOnline(status.isOnline);
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。
|
||||||
|
|
||||||
|
> 详细说明
|
||||||
|
>
|
||||||
|
> 你可以在这一章节了解更多关于 `useEffect` 的内容:[使用 Effect Hook](https://zh-hans.reactjs.org/docs/hooks-effect.html)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Hook 使用规则
|
||||||
|
|
||||||
|
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
|
||||||
|
|
||||||
|
- 只能在**函数最外层**调用 Hook。不要在循环、条件判断或者子函数中调用。
|
||||||
|
- 只能在 **React 的函数组件**中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)
|
||||||
|
|
||||||
|
同时,我们提供了 [linter 插件](https://www.npmjs.com/package/eslint-plugin-react-hooks)来自动执行这些规则。这些规则乍看起来会有一些限制和令人困惑,但是要让 Hook 正常工作,它们至关重要。
|
||||||
|
|
||||||
|
> 详细说明
|
||||||
|
>
|
||||||
|
> 你可以在这章节了解更多关于这些规则的内容:[Hook 使用规则](https://zh-hans.reactjs.org/docs/hooks-rules.html)。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 自定义 Hook (Hook的使用逻辑封装)
|
||||||
|
|
||||||
|
有时候我们会想要在组件之间重用一些状态逻辑。目前为止,有两种主流方案来解决这个问题:[高阶组件](https://zh-hans.reactjs.org/docs/higher-order-components.html)和 [render props](https://zh-hans.reactjs.org/docs/render-props.html)。自定义 Hook 可以让你在不增加组件的情况下达到同样的目的。
|
||||||
|
|
||||||
|
前面,我们介绍了一个叫 `FriendStatus` 的组件,它通过调用 `useState` 和 `useEffect` 的 Hook 来订阅一个好友的在线状态。假设我们想在另一个组件里重用这个订阅逻辑。
|
||||||
|
|
||||||
|
首先,我们把这个逻辑抽取到一个叫做 `useFriendStatus` 的自定义 Hook 里:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
// 逻辑抽取,方便复用
|
||||||
|
function useFriendStatus(friendID) {
|
||||||
|
// 每次调用时的state都是独立的
|
||||||
|
const [isOnline, setIsOnline] = useState(null);
|
||||||
|
|
||||||
|
function handleStatusChange(status) {
|
||||||
|
setIsOnline(status.isOnline);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
|
||||||
|
return () => {
|
||||||
|
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return isOnline;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
它将 `friendID` 作为参数,并返回该好友是否在线:
|
||||||
|
|
||||||
|
现在我们可以在两个组件中使用它:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
function FriendStatus(props) {
|
||||||
|
const isOnline = useFriendStatus(props.friend.id);
|
||||||
|
if (isOnline === null) {
|
||||||
|
return 'Loading...';
|
||||||
|
}
|
||||||
|
return isOnline ? 'Online' : 'Offline';
|
||||||
|
}
|
||||||
|
function FriendListItem(props) {
|
||||||
|
const isOnline = useFriendStatus(props.friend.id);
|
||||||
|
return (
|
||||||
|
<li style={{ color: isOnline ? 'green' : 'black' }}>
|
||||||
|
{props.friend.name}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**每个组件间的 state 是完全独立的。Hook 是一种复用*状态逻辑*的方式,它不复用 state 本身**。事实上 Hook 的每次*调用*都有一个完全独立的 state —— 因此你可以在单个组件中多次调用同一个自定义 Hook。
|
||||||
|
|
||||||
|
自定义 Hook 更像是一种约定而不是功能。**如果函数的名字以 “`use`” 开头并调用其他 Hook,我们就说这是一个自定义 Hook**。 `useSomething` 的命名约定可以让我们的 linter 插件在使用 Hook 的代码中找到 bug。
|
||||||
|
|
||||||
|
你可以创建涵盖各种场景的自定义 Hook,如表单处理、动画、订阅声明、计时器,甚至可能还有更多我们没想到的场景。我们很期待看到 React 社区会出现什么样的自定义 Hook。
|
||||||
|
|
||||||
|
> 详细说明
|
||||||
|
>
|
||||||
|
> 我们会在这一章节介绍更多关于自定义 Hook 的内容: [创建你自己的 Hook](https://zh-hans.reactjs.org/docs/hooks-custom.html)。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 其他 Hook
|
||||||
|
|
||||||
|
除此之外,还有一些使用频率较低的但是很有用的 Hook。比如,[`useContext`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext) 让你不使用组件嵌套就可以订阅 React 的 Context。
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
function Example() {
|
||||||
|
const locale = useContext(LocaleContext);
|
||||||
|
const theme = useContext(ThemeContext);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
另外 [`useReducer`](https://zh-hans.reactjs.org/docs/hooks-reference.html#usereducer) 可以让你通过 reducer 来管理组件本地的复杂 state。
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
function Todos() {
|
||||||
|
const [todos, dispatch] = useReducer(todosReducer); // ...
|
||||||
|
```
|
||||||
|
|
||||||
|
> 详细说明
|
||||||
|
>
|
||||||
|
> 你可以在这一章节了解更多关于所有内置 Hook 的内容:[Hook API 索引](https://zh-hans.reactjs.org/docs/hooks-reference.html)。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
# 02. 使用State Hook
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
function Example() {
|
||||||
|
// 声明一个叫 "count" 的 state 变量
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>You clicked {count} times</p>
|
||||||
|
<button onClick={() => setCount(count + 1)}>
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
我们将通过将这段代码与一个等价的 class 示例进行比较来开始学习 Hook。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 等价的Class组件示例
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
state 初始值为 `{ count: 0 }` ,当用户点击按钮后,我们通过调用 `this.setState()` 来增加 `state.count`。整个章节中都将使用该 class 的代码片段做示例。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Hook 在 class 内部是不起作用
|
||||||
|
|
||||||
|
**Hook 在 class 内部是不起作用的**。但你可以使用它们来取代 class 。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 调用 `useState` 方法的时候做了什么?
|
||||||
|
|
||||||
|
它定义一个 “state 变量”。我们的变量叫 `count`, 但是我们可以叫他任何名字,比如 `banana`。这是一种在函数调用时保存变量的方式 —— `useState` 是一种新方法,它与 class 里面的 `this.state` 提供的功能完全相同。**一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `useState` 需要哪些参数?
|
||||||
|
|
||||||
|
`useState()` 方法里面**唯一的参数就是初始 state**。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 `0` 作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用 `useState()` 两次即可。)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `useState` 方法的返回值是什么?
|
||||||
|
|
||||||
|
返回值为:当前 state 以及更新 state 的函数。这就是我们写 `const [count, setCount] = useState()` 的原因。这与 class 里面 `this.state.count` 和 `this.setState` 类似,唯一区别就是你需要成对的获取它们。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 理解useState
|
||||||
|
|
||||||
|
既然我们知道了 `useState` 的作用,我们的示例应该更容易理解了:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
function Example() {
|
||||||
|
// 声明一个叫 "count" 的 state 变量 const [count, setCount] = useState(0);
|
||||||
|
```
|
||||||
|
|
||||||
|
我们声明了一个叫 `count` 的 state 变量,然后把它设为 `0`。**React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。**我们可以通过调用 `setCount` 来更新当前的 `count`。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> 注意
|
||||||
|
>
|
||||||
|
> 你可能想知道:为什么叫 `useState` 而不叫 `createState`?
|
||||||
|
>
|
||||||
|
> “Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。在下一次重新渲染时,`useState` 返回给我们当前的 state。否则它就不是 “state”了!这也是 Hook 的名字*总是*以 `use` 开头的一个原因。我们将在后面的 [Hook 规则](https://zh-hans.reactjs.org/docs/hooks-rules.html)中了解原因。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 使用多个 state 变量
|
||||||
|
|
||||||
|
将 state 变量声明为一对 `[something, setSomething]` 也很方便,因为如果我们想使用多个 state 变量,它允许我们给不同的 state 变量取不同的名称:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
function ExampleWithManyStates() {
|
||||||
|
// 声明多个 state 变量
|
||||||
|
const [age, setAge] = useState(42);
|
||||||
|
const [fruit, setFruit] = useState('banana');
|
||||||
|
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
|
||||||
|
```
|
||||||
|
|
||||||
|
在以上组件中,我们有局部变量 `age`,`fruit` 和 `todos`,并且我们可以单独更新它们:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
function handleOrangeClick() {
|
||||||
|
// 和 this.setState({ fruit: 'orange' }) 类似
|
||||||
|
setFruit('orange');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
你**不必**使用多个 state 变量。**State 变量可以很好地存储对象和数组,因此,你仍然可以将相关数据分为一组**。然而,不像 class 中的 `this.setState`,**更新 state 变量总是*替换*它而不是合并它**。
|
||||||
|
|
||||||
|
我们[在 FAQ 中](https://zh-hans.reactjs.org/docs/hooks-faq.html#should-i-use-one-or-many-state-variables)提供了更多关于分离独立 state 变量的建议。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> 把所有 state 都放在同一个 `useState` 调用中,或是每一个字段都对应一个 `useState` 调用,这两方式都能跑通。当你在这两个极端之间找到平衡,然后把相关 state 组合到几个独立的 state 变量时,组件就会更加的可读。如果 state 的逻辑开始变得复杂,我们推荐 [用 reducer 来管理它](https://zh-hans.reactjs.org/docs/hooks-reference.html#usereducer),或使用自定义 Hook。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
# 03. 使用Effect Hook(副作用钩子)
|
||||||
|
|
||||||
|
如果你熟悉 React class 的生命周期函数,你可以把 `useEffect` Hook 看做 `componentDidMount`(挂载完成),`componentDidUpdate`(更新完成) 和 `componentWillUnmount`(即将销毁前) 这三个函数的组合。
|
||||||
|
|
||||||
|
> Did : 做了... Will: 将要...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用**。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
在 React 组件中有两种常见副作用操作:**需要清除的 **和 **不需要清除的**。我们来更仔细地看一下他们之间的区别。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 无需清除的 effect
|
||||||
|
|
||||||
|
有时候,我们只想**在 React 更新 DOM 之后运行一些额外的代码。**比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 使用 class 的示例
|
||||||
|
|
||||||
|
在 React 的 class 组件中,`render` 函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。
|
||||||
|
|
||||||
|
这就是为什么在 React class 中,我们把副作用操作放到 `componentDidMount` 和 `componentDidUpdate` 函数中。回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
class Example extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
count: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOM挂载了
|
||||||
|
componentDidMount() {
|
||||||
|
document.title = `You clicked ${this.state.count} times`;
|
||||||
|
}
|
||||||
|
// DOM更新了
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意,**在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。**
|
||||||
|
|
||||||
|
这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。**从概念上说,我们希望它在每次渲染之后执行** —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
|
||||||
|
|
||||||
|
现在让我们来看看如何使用 `useEffect` 执行相同的操作。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 使用 Hook 的示例
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
function Example() {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => { // 每次渲染组件DOM后执行此回调函数(即,挂载完成和更新完成生命周期)
|
||||||
|
document.title = `You clicked ${count} times`;
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>You clicked {count} times</p>
|
||||||
|
<button onClick={() => setCount(count + 1)}>
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`useEffect` 做了什么?** 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
|
||||||
|
|
||||||
|
**为什么在组件内部调用 `useEffect`?** 将 `useEffect` 放在组件内部让我们可以在 effect 中直接访问 `count` state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
|
||||||
|
|
||||||
|
**`useEffect` 会在每次渲染后都执行吗?** 是的,默认情况下,它在第一次渲染之后*和*每次更新之后都会执行。(我们稍后会谈到[如何控制它](https://zh-hans.reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects)。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 不同之处
|
||||||
|
|
||||||
|
与 `componentDidMount` 或 `componentDidUpdate` 不同,使用 `useEffect` 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 [`useLayoutEffect`](https://zh-hans.reactjs.org/docs/hooks-reference.html#uselayouteffect) Hook 供你使用,其 API 与 `useEffect` 相同。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 需要清除的 effect
|
||||||
|
|
||||||
|
之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如**订阅外部数据源**。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 使用 Class 的示例
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
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 的示例
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
function FriendStatus(props) {
|
||||||
|
const [isOnline, setIsOnline] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => { // 每次渲染组件DOM后执行此回调函数(即,挂载完成和更新完成生命周期)
|
||||||
|
function handleStatusChange(status) {
|
||||||
|
setIsOnline(status.isOnline);
|
||||||
|
}
|
||||||
|
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
|
||||||
|
// 每次重新渲染前都会执行(class的 componentWillUnmount 只在卸载组件执行)
|
||||||
|
return function cleanup() { // 函数名非必需,可使用箭头函数
|
||||||
|
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isOnline === null) {
|
||||||
|
return 'Loading...';
|
||||||
|
}
|
||||||
|
return isOnline ? 'Online' : 'Offline';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 使用 Effect 的提示
|
||||||
|
|
||||||
|
### 使用Hook的目的
|
||||||
|
|
||||||
|
使用 Hook 其中一个[目的](https://zh-hans.reactjs.org/docs/hooks-intro.html#complex-components-become-hard-to-understand)就是要解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 提示: 使用多个 Effect 实现关注点分离
|
||||||
|
|
||||||
|
可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
function FriendStatusWithCounter(props) {
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = `You clicked ${count} times`;
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 解释: 为什么每次更新的时候都要运行 Effect
|
||||||
|
|
||||||
|
如果你已经习惯了使用 class,那么你或许会疑惑**为什么 effect 的清除阶段在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次**。让我们看一个实际的例子,看看为什么这个设计可以帮助我们创建 bug 更少的组件。
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
**忘记正确地处理 `componentDidUpdate` 是 React 应用中常见的 bug 来源**。
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 提示: 通过跳过 Effect 进行性能优化
|
||||||
|
|
||||||
|
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。
|
||||||
|
|
||||||
|
如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React **跳过**对 effect 的调用,只要传递数组作为 `useEffect` 的第二个可选参数即可:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = `You clicked ${count} times`;
|
||||||
|
}, [count]); // 仅在 count 更改时更新
|
||||||
|
```
|
||||||
|
|
||||||
|
对于有清除操作的 effect 同样适用:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
useEffect(() => {
|
||||||
|
function handleStatusChange(status) {
|
||||||
|
setIsOnline(status.isOnline);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
|
||||||
|
return () => {
|
||||||
|
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
|
||||||
|
};
|
||||||
|
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
|
||||||
|
```
|
||||||
|
|
||||||
|
未来版本,可能会在构建时自动添加第二个参数。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**注意:**
|
||||||
|
|
||||||
|
1. 如果你要使用此优化方式,请确保数组中包含了**所有外部作用域中会随时间变化并且在 effect 中使用的变量**,否则你的代码会引用到先前渲染中的旧变量。
|
||||||
|
|
||||||
|
2. 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组(`[]`)作为第二个参数。
|
||||||
|
|
||||||
|
3. 如果你传入了一个空数组(`[]`),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入 `[]` 作为第二个参数更接近大家更熟悉的 `componentDidMount` 和 `componentWillUnmount` 思维模式,但我们有[更好的](https://zh-hans.reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies)[方式](https://zh-hans.reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often)来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 `useEffect`,因此会使得额外操作很方便。
|
||||||
|
|
||||||
|
4. 我们推荐启用 [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks#installation) 中的 [`exhaustive-deps`](https://github.com/facebook/react/issues/14920) 规则。此规则会在添加错误依赖时发出警告并给出修复建议。
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# 04. Hook规则
|
||||||
|
|
||||||
|
Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循**两条规则**。我们提供了一个 [linter 插件](https://www.npmjs.com/package/eslint-plugin-react-hooks)来强制执行这些规则:
|
||||||
|
|
||||||
|
## 只在最顶层使用 Hook
|
||||||
|
|
||||||
|
不要在循环,条件或嵌套函数中调用 Hook, 确保**总是在你的 React 函数的最顶层以及任何 return 之前调用他们**。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
1. React 怎么知道哪个 state 对应哪个 `useState`?答案是 React 靠的是 Hook 调用的顺序
|
||||||
|
|
||||||
|
2. 如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的*内部*:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
useEffect(function persistForm() {
|
||||||
|
// 👍 将条件判断放置在 effect 中
|
||||||
|
if (name !== '') {
|
||||||
|
localStorage.setItem('formData', name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 只在 React 函数中调用 Hook
|
||||||
|
|
||||||
|
**不要在普通的 JavaScript 函数中调用 Hook。**你可以:
|
||||||
|
|
||||||
|
- ✅ 在 React 的函数组件中调用 Hook
|
||||||
|
- ✅ 在自定义 Hook 中调用其他 Hook
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## ESLint 插件
|
||||||
|
|
||||||
|
我们发布了一个名为 [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) 的 ESLint 插件来强制执行这两条规则。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install eslint-plugin-react-hooks --save-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 你的 ESLint 配置
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
// ...
|
||||||
|
"react-hooks"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
// ...
|
||||||
|
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
|
||||||
|
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
# 05. 自定义 Hook
|
||||||
|
|
||||||
|
**通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。**
|
||||||
|
|
||||||
|
目前为止,在 React 中有两种流行的方式来共享组件之间的状态逻辑: [render props](https://zh-hans.reactjs.org/docs/render-props.html) 和[高阶组件](https://zh-hans.reactjs.org/docs/higher-order-components.html),现在让我们来看看 Hook 是如何在让你不增加组件的情况下解决相同问题的。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 提取自定义 Hook
|
||||||
|
|
||||||
|
**自定义 Hook 是一个函数,其名称以 “`use`” 开头,函数内部可以调用其他的 Hook。** 例如,下面的 `useFriendStatus` 是我们第一个自定义的 Hook:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它**就像一个正常的函数**。但是它的名字应该始终以 `use` 开头,这样可以一眼看出其符合 [Hook 的规则](https://zh-hans.reactjs.org/docs/hooks-rules.html)。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 使用自定义 Hook
|
||||||
|
|
||||||
|
现在我们已经把这个逻辑提取到 `useFriendStatus` 的自定义 Hook 中,然后就可以*使用它了:*
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
function FriendStatus(props) {
|
||||||
|
const isOnline = useFriendStatus(props.friend.id);
|
||||||
|
if (isOnline === null) {
|
||||||
|
return 'Loading...';
|
||||||
|
}
|
||||||
|
return isOnline ? 'Online' : 'Offline';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
|
||||||
|
function FriendListItem(props) {
|
||||||
|
const isOnline = useFriendStatus(props.friend.id);
|
||||||
|
return (
|
||||||
|
<li style={{ color: isOnline ? 'green' : 'black' }}>
|
||||||
|
{props.friend.name}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**这段代码等价于原来的示例代码吗?**等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。**自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。**
|
||||||
|
|
||||||
|
**自定义 Hook 必须以 “`use`” 开头吗?**必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 [Hook 的规则](https://zh-hans.reactjs.org/docs/hooks-rules.html)。
|
||||||
|
|
||||||
|
**在两个组件中使用相同的 Hook 会共享 state 吗?**不会。自定义 Hook 是一种重用*状态逻辑*的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
|
||||||
|
|
||||||
|
**自定义 Hook 如何获取独立的 state?**每次*调用* Hook,它都会获取独立的 state。由于我们直接调用了 `useFriendStatus`,从 React 的角度来看,我们的组件只是调用了 `useState` 和 `useEffect`。 正如我们在[之前章节](https://zh-hans.reactjs.org/docs/hooks-effect.html#tip-use-multiple-effects-to-separate-concerns)中[了解到的](https://zh-hans.reactjs.org/docs/hooks-state.html#tip-using-multiple-state-variables)一样,我们可以在一个组件中多次调用 `useState` 和 `useEffect`,它们是完全独立的。
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
### 小总结
|
||||||
|
|
||||||
|
1. 自定义hook就像一个普通函数,内部包含可复用的组件逻辑。
|
||||||
|
2. 函数名以`use`开头。
|
||||||
|
3. `state`是独立的。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue