From 7a662a9288b424965c79a30b635697acb5c35307 Mon Sep 17 00:00:00 2001 From: xugaoyi <894072666@qq.com> Date: Tue, 6 Apr 2021 19:12:49 +0800 Subject: [PATCH] =?UTF-8?q?blog:=20=E6=9B=B4=E6=96=B0React=20hook=E7=AC=94?= =?UTF-8?q?=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/《React》笔记/03.Hook/01.Hook.md | 54 ---- docs/《React》笔记/03.Hook/01.Hook概述.md | 296 ++++++++++++++++++ .../03.Hook/02.使用State Hook.md | 144 +++++++++ .../03.Hook/03.使用Effect Hook.md | 269 ++++++++++++++++ docs/《React》笔记/03.Hook/04.Hook规则.md | 61 ++++ docs/《React》笔记/03.Hook/05.自定义 Hook.md | 83 +++++ 6 files changed, 853 insertions(+), 54 deletions(-) delete mode 100644 docs/《React》笔记/03.Hook/01.Hook.md create mode 100644 docs/《React》笔记/03.Hook/01.Hook概述.md create mode 100644 docs/《React》笔记/03.Hook/02.使用State Hook.md create mode 100644 docs/《React》笔记/03.Hook/03.使用Effect Hook.md create mode 100644 docs/《React》笔记/03.Hook/04.Hook规则.md create mode 100644 docs/《React》笔记/03.Hook/05.自定义 Hook.md diff --git a/docs/《React》笔记/03.Hook/01.Hook.md b/docs/《React》笔记/03.Hook/01.Hook.md deleted file mode 100644 index 671cfb3..0000000 --- a/docs/《React》笔记/03.Hook/01.Hook.md +++ /dev/null @@ -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 ( -
-

You clicked {count} times

- -
- ); -} -``` - - - diff --git a/docs/《React》笔记/03.Hook/01.Hook概述.md b/docs/《React》笔记/03.Hook/01.Hook概述.md new file mode 100644 index 0000000..a0ecac2 --- /dev/null +++ b/docs/《React》笔记/03.Hook/01.Hook概述.md @@ -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 ( +
+

You clicked {count} times

+ +
+ ); +} +``` + + + +### 声明多个 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 ( +
+

You clicked {count} times

+ +
+ ); +} +``` + +当你调用 `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 ( +
  • + {props.friend.name} +
  • + ); +} +``` + +**每个组件间的 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)。 + + + + + + + + + + + + + + + diff --git a/docs/《React》笔记/03.Hook/02.使用State Hook.md b/docs/《React》笔记/03.Hook/02.使用State Hook.md new file mode 100644 index 0000000..681db3e --- /dev/null +++ b/docs/《React》笔记/03.Hook/02.使用State Hook.md @@ -0,0 +1,144 @@ +# 02. 使用State Hook + +```jsx +import React, { useState } from 'react'; + +function Example() { + // 声明一个叫 "count" 的 state 变量 + const [count, setCount] = useState(0); + + return ( +
    +

    You clicked {count} times

    + +
    + ); +} +``` + +我们将通过将这段代码与一个等价的 class 示例进行比较来开始学习 Hook。 + + + +## 等价的Class组件示例 + +```jsx +class Example extends React.Component { + constructor(props) { + super(props); + this.state = { + count: 0 + }; + } + + render() { + return ( +
    +

    You clicked {this.state.count} times

    + +
    + ); + } +} +``` + +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。 + + + + + + + + + + + + + diff --git a/docs/《React》笔记/03.Hook/03.使用Effect Hook.md b/docs/《React》笔记/03.Hook/03.使用Effect Hook.md new file mode 100644 index 0000000..3353585 --- /dev/null +++ b/docs/《React》笔记/03.Hook/03.使用Effect Hook.md @@ -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 ( +
    +

    You clicked {this.state.count} times

    + +
    + ); + } +} +``` + +注意,**在这个 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 ( +
    +

    You clicked {count} times

    + +
    + ); +} +``` + +**`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) 规则。此规则会在添加错误依赖时发出警告并给出修复建议。 + + \ No newline at end of file diff --git a/docs/《React》笔记/03.Hook/04.Hook规则.md b/docs/《React》笔记/03.Hook/04.Hook规则.md new file mode 100644 index 0000000..6702ea3 --- /dev/null +++ b/docs/《React》笔记/03.Hook/04.Hook规则.md @@ -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 的依赖 + } +} +``` + diff --git a/docs/《React》笔记/03.Hook/05.自定义 Hook.md b/docs/《React》笔记/03.Hook/05.自定义 Hook.md new file mode 100644 index 0000000..a33185f --- /dev/null +++ b/docs/《React》笔记/03.Hook/05.自定义 Hook.md @@ -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 ( +
  • + {props.friend.name} +
  • + ); +} +``` + +**这段代码等价于原来的示例代码吗?**等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。**自定义 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`是独立的。 + + +