1、React Hooks 介绍

  1. Hooks 是什么
  2. 为什么要有 Hooks

1.1 Hooks 是什么

  • ​Hooks​​:钩子、钓钩、钩住
  • ​Hooks​​ 是React v16.8中的新增功能
  • 作用:为函数组件提供状态、生命周期等原本 class 组件中提供的 React 功能
  • 可以理解为通过 Hooks 为函数组件钩入 class 组件的特性
  • 注意:Hooks 只能在函数组件中使用,自此,函数组件成为 React 的新宠儿

React v16.8 版本前后,组件开发模式的对比:

  • React v16.8 以前: class 组件(提供状态) + 函数组件(展示内容)
  • React v16.8 及其以后:
  1. class 组件(提供状态) + 函数组件(展示内容)
  2. Hooks(提供状态) + 函数组件(展示内容)
  3. 混用以上两种方式:部分功能用 class 组件,部分功能用 Hooks+函数组件

注意1:虽然有了 Hooks,但 React 官方并没有计划从 React 库中移除 class。
注意2:有了 Hooks 以后,不能再把函数组件称为无状态组件了,因为 Hooks 为函数组件提供了状态。

1.2 为什么要有 Hooks

两个角度:1 组件的状态逻辑复用 2 class 组件自身的问题

  1. 组件的状态逻辑复用:
  • 在 Hooks 之前,组件的状态逻辑复用经历了:mixins(混入)、HOCs(高阶组件)、render-props 等模式。
  • (早已废弃)mixins 的问题:1 数据来源不清晰 2 命名冲突。
  • HOCs、render-props 的问题:重构组件结构,导致组件形成 JSX 嵌套地狱问题。
  1. class 组件自身的问题:
  • 选择:函数组件和 class 组件之间的区别以及使用哪种组件更合适
  • 需要理解 class 中的 this 是如何工作的
  • 相互关联且需要对照修改的代码被拆分到不同生命周期函数中
  • componentDidMount -> window.addEventListener(‘resize’, this.fn)
  • componentWillUnmount -> window.addEventListener(‘resize’, this.fn)
  • 相比于函数组件来说,不利于代码压缩和优化,也不利于 TS 的类型推导

正是由于 React 原来存在的这些问题,才有了 Hooks 来解决这些问题

1.3 hooks的优势

由于原来 React 中存在的问题,促使 React 需要一个更好的自带机制来实现组件状态逻辑复用。

  1. Hooks 只能在函数组件中使用,避免了 class 组件的问题
  2. 复用组件状态逻辑,而无需更改组件层次结构
  3. 根据功能而不是基于生命周期方法强制进行代码分割
  4. 抛开 React 赋予的概念来说,Hooks 就是一些普通的函数
  5. 具有更好的 TS 类型推导
  6. tree- - shaking 友 好,打包时去掉未引用的代码
  7. 更好的压 缩

项目开发中,Hooks 的采用策略:

  • 不推荐直接使用 Hooks 大规模重构现有组件
  • 推荐:新功能用 Hooks,复杂功能实现不了的,也可以继续用 class
  • 找一个功能简单、非核心功能的组件开始使用 hooks

1.4 前面学习的 React 知识是有用的

class 组件相关的 API 不用了,比如:

  • ​class Hello extends Component​
  • ​componentDidMount​​​、​​componentDidUpdate​​​、​​componentWillUnmount​
  • ​this​​ 相关的用法

原来学习的内容还是要用的,比如:

  • JSX:​​{}​​​、​​onClick={handleClick}​​、条件渲染、列表渲染、样式处理等
  • 组件:函数组件、组件通讯
  • 路由
  • React 开发理念:​​单向数据流​​​、​​状态提升​​ 等
  • 解决问题的思路、技巧、常见错误的分析等上

2、useState Hook

2.1 概述

问题:Hook 是什么? 一个 Hook 就是一个特殊的函数,让你在函数组件中获取状态等 React 特性
使用模式:函数组件 + Hooks
特点:从名称上看,Hook 都以 use 开头

2.2 useState Hook 的基本使用

  • 使用场景:当你想要在函数组件中,使用组件状态时,就要使用useStateHook 了
  • 作用:为函数组件提供状态(state)
  • 使用步骤:
  1. 导入​​useState​​ 函数
  2. 调用​​useState​​ 函数,并传入状态的初始值
  3. 从​​useState​​ 函数的返回值中,拿到状态和修改状态的函数
  4. 在 JSX 中展示状态
  5. 在按钮的点击事件中调用修改状态的函数,来更新状态
import { useState } from 'react'
const Count = () => {
// 返回值是一个数组
const stateArray = useState(0)
// 状态值 -> 0
const state = stateArray[0]
// 修改状态的函数
const setState = stateArray[1]

return (
<div>
{/* 展示状态值 */}
<h1>useState Hook -> {state}</h1>
{/* 点击按钮,让状态值 +1 */}
<button onClick={() => setState(state + 1)}>+1</button>
</div>
)
}
  • 参数:状态初始值。比如,传入 0 表示该状态的初始值为 0
  • 注意:此处的状态可以是任意值(比如,数值、字符串等),而 class 组件中的 state 必须是对象
  • 返回值:数组,包含两个值:1 状态值(state) 2 修改该状态的函数(setState)

2.3 使用数组解构简化

比如,要获取数组中的元素:

  1. 原始方式:索引访问
const arr = ['aaa', 'bbb']
const a = arr[0] // 获取索引为 0 的元素
const b = arr[1] // 获取索引为 1 的元素
  1. 简化方式:数组解构
  • 相当于创建了两个变量(可以是任意的变量名称)分别获取到对应索引的数组元素
const arr = ['aaa', 'bbb']
const [a, b] = arr
// a => arr[0]
// b => arr[1]

const [state, setState] =
  • 使用数组解构简化​​useState​​ 的使用
  • 约定:修改状态的函数名称以 set 开头,后面跟上状态的名称
// 解构出来的名称可以是任意名称

const [state, setState] = useState(0)
const [age, setAge] = useState(0)
const [count, setCount] = useState(0)

2.4 状态的读取和修改

状态的使用:1 读取状态 2 修改状态

  1. 读取状态:该方式提供的状态,是函数内部的局部变量,可以在函数内的任意位置使用
  2. 修改状态:
  • ​setCount(newValue)​​ 是一个函数,参数表示:新的状态值
  • 调用该函数后,将使用新的状态值替换旧值
  • 修改状态后,因为状态发生了改变,所以,该组件会重新渲染

2.5 组件的更新过程

函数组件使用 useState hook 后的执行过程,以及状态值的变化:

  • 组件第一次渲染:
  1. 从头开始执行该组件中的代码逻辑
  2. 调用​​useState(0)​​ 将传入的参数作为状态初始值,即:0
  3. 渲染组件,此时,获取到的状态 count 值为: 0
  • 组件第二次渲染:
  1. 点击按钮,调用​​setCount(count + 1)​​ 修改状态,因为状态发生改变,所以,该组件会重新渲染
  2. 组件重新渲染时,会再次执行该组件中的代码逻辑
  3. 再次调用​​useState(0)​​,此时React 内部会拿到最新的状态值而非初始值,比如,该案例中最新的状态值为 1
  4. 再次渲染组件,此时,获取到的状态 count 值为:1

注意:useState 的初始值(参数)只会在组件第一次渲染时生效

也就是说,以后的每次渲染,useState 获取到都是最新的状态值。React 组件会记住每次最新的状态值!

2.6 为函数组件添加多个状态

问题:如果一个函数组件需要多个状态,该如何处理?
回答:调用 ​​​useState​​​ Hook 多次即可,每调用一次 useState Hook 可以提供一个状态。
注意:useState Hook 多次调用返回的 [state, setState] 相互之间,互不影响。

2.7 hooks 的使用规则

注意:React Hooks 只能直接出现在 函数组件 中,不能嵌套在 if/for/其他函数中

否则就会报错:React Hook “useState” is called conditionally. React Hooks must be called in the exact same order in every component render

React 的 useState 这个 Hook 被条件性(放在一个条件判断中)的调用了。

React Hooks 必须要每次组件渲染时,按照相同的顺序来调用所有的 Hooks。

  • 为什么会有这样的规则? 因为 React 是按照 Hooks 的调用顺序来识别每一个 Hook,如果每次调用的顺序不同,导致 React 无法知道是哪一个 Hook
  • 通过开发者工具可以查看到。

3、useEffect Hook

  1. side effect - 副作用
  2. useEffect 的基本使用
  3. useEffect 的依赖
  4. useEffect 发送请求

3.1 side effect - 副作用

使用场景:当你想要在函数组件中,处理副作用(side effect)时,就要使用 useEffect Hook 了
作用:处理函数组件中的副作用(side effect)

问题:副作用(side effect)是什么?
回答:在计算机科学中,如果一个函数或其他操作修改了其局部环境之外的状态变量值,那么它就被称为有副作用
类比,对于 999 感冒灵感冒药来说:

  • )作用:用于感冒引起的头痛,发热,鼻塞,流涕,咽痛等
  • 副作用:可见困倦、嗜睡、口渴、虚弱感

理解:副作用是相对于主作用来说的,一个功能(比如,函数)除了主作用,其他的作用就是副作用
对于 React 组件来说,主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用(比如,手动修改 DOM)

React 组件的公式:UI = f(state)

常见的副作用(side effect)

  • 数据(Ajax)请求、手动修改 DOM、localStorage 操作等

3.2 useEffect 的基本使用

使用场景:当你想要在函数组件中,处理副作用(side effect)时,就要使用 useEffect Hook 了
作用:处理函数组件中的副作用(side effect)
注意:在实际开发中,副作用是不可避免的。因此,react 专门提供了 useEffect Hook 来处理函数组件中的副作用

import { useEffect } from 'react'

useEffect(function effect() {
document.title = `当前已点击 ${count} 次`
})

useEffect(() => {
document.title = `当前已点击 ${count} 次`
})

解释:

  • 参数:回调函数(称为effect),就是在该函数中写副作用代码
  • 执行时机:该 effect 会在组件渲染后以及组件更新后执行
  • 相当于componentDidMount + componentDidUpdate

3.3 useEffect 的依赖

  • 问题:如果组件中有另外一个状态,另一个状态更新时,刚刚的 effect 回调,也会执行
  • 性能优化:跳过不必要的执行,只在 count 变化时,才执行相应的 effect
useEffect(() => {
document.title = `当前已点击 ${count} 次`
}, [count])

解释:

  • 第二个参数:可选的,可省略;也可以传一个数组,数组中的元素可以成为依赖项(deps)
  • 该示例中表示:只有当 count 改变时,才会重新执行该 effect

3.4 useEffect 的依赖是一个空数组

useEffect 的第二个参数,还可以是一个空数组([]),表示只在组件第一次渲染后执行 effect
使用场景:1 事件绑定 2 发送请求获取数据 等

useEffect(() => {
const handleResize = () => {}
window.addEventListener('resize', handleResize)
}, [])

解释:

  • 该 effect 只会在组件第一次渲染后执行,因此,可以执行像事件绑定等只需要执行一次的操作
  • 此时,相当于 class 组件的 componentDidMount 钩子函数的作用
  • 跟 useState Hook 一样,一个组件中也可以调用 useEffect Hook 多次
  • 推荐:一个 useEffect 只处理一个功能,有多个功能时,使用多次 useEffect

3.5 总结 useEffect 的使用

// 触发时机:1 第一次渲染会执行 2 每次组件重新渲染都会再次执行
// componentDidMount + ComponentDidUpdate
useEffect(() => {})

// componentDidMount
// 触发时机:只在组件第一次渲染时执行
useEffect(() => {}, [])

// componentDidMount + componentDidUpdate(判断)
// 触发时机:1 第一次渲染会执行 2 当 count 变化时会再次执行
useEffect(() => {}, [count])

3.6 不要对useEffect的依赖项撒谎

const App = () => {
const [count, setCount] = useState(0)
useEffect(() => {
document.title = '点击了' + count + '次'
}, [])
return (
<div>
<h1>计数器:{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
<hr />
</div>
)
}

useEffect完全指南:https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/

3.7 useEffect 清理副作用

有时候,我们只想**在 React 更新 DOM 之后运行一些额外的代码。**比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。

还有一些副作用是需要清除的。例如订阅外部数据源, 开启定时器,注册事件。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!

问题:如何在组件卸载时,解绑事件?此时,就用到 effect 的返回值了

useEffect(() => {
const handleResize = () => {}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])

解释:

  • effect 的返回值也是可选的,可省略。也可以返回一个清理函数,用来执行事件解绑等清理操作
  • 清理函数的执行时机:1 组件卸载时 2 effect 重新执行前
  • 此时,相当于 class 组件的 componentWillUnmount 钩子函数的作用
  • 推荐:一个 useEffect 只处理一个功能,有多个功能时,使用多次 useEffect
  • 优势:根据业务逻辑来拆分,相同功能的业务逻辑放在一起,而不是根据生命周期方法名称来拆分代码
  • 编写代码时,关注点集中;而不是上下翻滚来查看代码

3.8 将事件处理程序放在 useEffect 内部

// 1 将 resize 事件处理程序放在 effect 回调中,当前这个代码是没有问题的
useEffect(() => {
const handleResize = () => {
console.log('window 窗口大小改变了')
}
window.addEventListener('resize', handleResize)

return () => {
window.removeEventListener('resize', handleResize)
}
}, [])

// 2 将 resize 事件处理程序拿到 useEffect 的外部,当前这个代码是没有问题的
const handleResize = () => {
console.log('window 窗口大小改变了')
}

useEffect(() => {
window.addEventListener('resize', handleResize)

return () => {
window.removeEventListener('resize', handleResize)
}
}, [])

// 3 有依赖项的情况:
useEffect(() => {
// resize 事件的处理程序
const handleResize = () => {
console.log('window 窗口大小改变了', count)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [count])

// 注意:此处的代码,会给一些警告!!! 不要按照这种方式写代码!!!
// 4 如果将 handleResize 放到了 useEffect 外部,React 会给以警告:
// 要么将 handleResize 放到 useEffect 中
// 要么使用 useCallback 这个 hook 来包裹 handleResize
// resize 事件的处理程序
const handleResize = () => {
console.log('window 窗口大小改变了', count)
}
useEffect(() => {
console.log('useeffect 执行了')
window.addEventListener('resize', handleResize)

return () => {
window.removeEventListener('resize', handleResize)
}
}, [handleResize])

// 总结以上几种情况,推荐:在给 window 绑定事件时,将 事件处理程序放在 useEffect 内部。

3.9 useEffect 发送请求

在组件中,使用 useEffect Hook 发送请求获取数据(side effect):

useEffect(() => {
const loadData = async () => {}
loadData()
}, [])

解释:

  • 注意:effect 只能是一个同步函数,不能使用 async
  • 因为 effect 的返回值应该是一个清理函数,React 会在组件卸载或者 effect 的依赖项变化时重新执行
  • 但如果 effect 是 async 的,此时返回值是 Promise 对象。这样的话,就无法保证清理函数被立即调用
  • 如果延迟调用清理函数,也就没有机会忽略过时的请求结果或取消请求
  • 为了使用 async/await 语法,可以在 effect 内部创建 async 函数,并调用
// 错误演示:

// 不要给 effect 添加 async
useEffect(async () => {}, [])
// https://github.com/facebook/react/issues/14326#issuecomment-441680293

useEffect(() => {
// 是否取消本次请求
let didCancel = false

async function fetchMyAPI() {
let url = 'http://something/' + productId
let config = {}
const response = await myFetch(url)
// 如果开启其他请求,就忽略本次(过时)的请求结果
if (!didCancel) {
console.log(response)
}
}

fetchMyAPI()
return () => { didCancel = true } // 取消本次请求
}, [productId])

3.10 useEffect循环报错问题

useEffect(() => {
const getList = async () => {
const res = await axios.get('http://geek.itheima.net/v1_0/channels')
setList(res.data.data.channels)
}
console.log(list)
getList()
}, [list])

4、自定义hooks

除了使用内置的 Hooks 之外,还可以创建自己的 Hooks(自定义 Hooks)。

使用场景:将组件状态逻辑提取到可重用的函数(自定义 Hooks)中,实现状态逻辑复用。

内置 Hooks 为函数组件赋予了 class 组件的功能;在此之上,自定义 Hooks 针对不同组件实现不同状态逻辑复用。

  • 自定义 Hooks 是一个函数,约定函数名称必须以 use 开头,React 就是通过函数名称是否以 use 开头来判断是不是 Hooks
  • Hooks 只能在函数组件中或其他自定义 Hooks 中使用,否则,会报错!
  • 自定义 Hooks 用来提取组件的状态逻辑,根据不同功能可以有不同的参数和返回值(就像使用普通函数一样)