Skip to main content

[React] hooks-useState/useEffect

React Hooks 介绍

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

Hooks 是什么

  • Hooks:钩子、钓钩、钩住
  • HooksReact 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 为函数组件提供了状态。

为什么要有 Hooks

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

  1. 组件的状态逻辑复用:

    • 在 Hooks 之前,组件的状态逻辑复用经历了:mixins(混入)、HOCs(高阶组件)、render-props 等模式。
    • (早已废弃)mixins 的问题:1 数据来源不清晰 2 命名冲突。
    • HOCs、render-props 的问题:重构组件结构,导致组件形成 JSX 嵌套地狱问题。
  2. class 组件自身的问题:

    • 选择:函数组件和 class 组件之间的区别以及使用哪种组件更合适
    • 需要理解 class 中的 this 是如何工作的
    • 相互关联且需要对照修改的代码被拆分到不同生命周期函数中
      • componentDidMount -> window.addEventListener('resize', this.fn)
      • componentWillUnmount -> window.addEventListener('resize', this.fn)
  • 相比于函数组件来说,不利于代码压缩和优化,也不利于 TS 的类型推导

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

hooks的优势

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

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

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

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

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

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

  • class Hello extends Component
  • componentDidMountcomponentDidUpdatecomponentWillUnmount
  • this 相关的用法

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

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

useState Hook

概述

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

useState Hook 的基本使用

  • 使用场景:当你想要在函数组件中,使用组件状态时,就要使用 useState Hook 了
  • 作用:为函数组件提供状态(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)

使用数组解构简化

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

  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] = arr
  • 使用数组解构简化 useState 的使用
    • 约定:修改状态的函数名称以 set 开头,后面跟上状态的名称
// 解构出来的名称可以是任意名称

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

状态的读取和修改

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

  1. 读取状态:该方式提供的状态,是函数内部的局部变量,可以在函数内的任意位置使用

  2. 修改状态:

  • setCount(newValue) 是一个函数,参数表示:新的状态值
  • 调用该函数后,将使用新的状态值替换旧值
  • 修改状态后,因为状态发生了改变,所以,该组件会重新渲染

组件的更新过程

函数组件使用 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 组件会记住每次最新的状态值!

为函数组件添加多个状态

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

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
  • 通过开发者工具可以查看到.

useEffect Hook

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

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 操作等
// 不带副作用的情况:
// 该函数的(主)作用:计算两个数的和
function fn(a, b) {
return a + b
}

// 带副作用的情况:
let c = 1
function fn(a, b) {
// 因为此处修改函数外部的变量值,而这一点不是该函数的主作用,因此,就是:side effect(副作用)
c = 2
return a + b
}

// 带副作用的情况:
function fn(a, b) {
// 因为 console.log 会导致控制台打印内容,所以,也是对外部产生影响,所以,也是:副作用
console.log(a)
return a + b
}

// 没有副作用:
function fn(obj) {
return obj.name
}

// 有副作用:
function fn(obj) {
// 此处直接修改了参数的值,也是一个副作用
obj.name = '大飞哥'
return obj.name
}
const o = { name: '小马哥' }
fn(o)

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 会在每次组件更新(DOM更新)后执行
  • 相当于componentDidMount + componentDidUpdate

useEffect 的依赖

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

解释:

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

useEffect 的依赖是一个空数组

useEffect 的第二个参数,还可以是一个空数组([]),表示只在组件第一次渲染后执行 effect. 因为它依赖一个空值, 空值是永远不会发生变化的. 意味着只执行一次
使用场景:1 事件绑定 2 发送请求获取数据 等

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

解释:

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

useEffect 清理副作用

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

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

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

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

// 返回的函数称为清理副作用的函数
// 这个函数会在组件销毁的时候执行 componentWillUnmount
// 以及每次渲染的时候都会执行
// 这就是为什么React会在执行当前effect之前对上一个effect进行清除

解释:

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

总结 useEffect 的使用

1. useEffect的语法
// 触发时机:1 第一次渲染会执行 2 每次组件重新渲染都会再次执行
// 等价于componentDidMount + componentDidUpdate
useEffect(() => {})

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

// 触发时机:1 第一次渲染会执行 2 当 依赖项 变化时会再次执行
useEffect(() => {}, [依赖项])


2. 清理副作用的语法
// 当组件销毁的时候, 以及每次回调函数执行之前(除了第一次, 因为第一次来还没有副作用), 都会清理副作用
useEffect(() => {
// xxxx
return () => {
// 清理副作用
}
})

// 当组件销毁的时候, 清理副作用
useEffect(() => {
// xxxx
return () => {
// 清理副作用
}
}, [])

useEffect 发送请求

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

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

解释:

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

effect 只能是一个同步函数,不能使用 async
Effect callbacks are synchronous to prevent race conditions.
Put the async function inside: useEffect(() => {async function fetchData() { // You can await here const response = await MyAPI.getData(someId); // ... } fetchData();}, [someId]);

// 错误演示:

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

// 实例1
useEffect(() => {
const getList = async () => {
const res = await axios.get("xxx");
setList(res);
};
getList();
}, []);

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

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

fetchMyAPI();
return () => { didCancel = true; }; // Remember if we start fetching something else
}, [productId]);


为何effect不能是异步的

effect将来是可能有返回值的, 
这个返回值可能是个函数, 用来清理副作用
async函数的返回结果是个promise,
那么
async function fn() { return () => {} }
fn() // 返回的是个promise
本来清理副作用的话, 把返回的函数调用一下即可,
promise不能直接调用,
而是promise.then(fn => fn())
所以清理副作用会导致清理不及时


导致effect的死循环的演示

  const [list, setList] = useState([]);
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在更新后的结果, 写了console.log(list)这句代码,
控制台报错 React Hook useEffect has a missing dependecy: 'list'
在依赖项加入list后, 发生死循环

因为依赖了list, 而依赖项发生变化, 传入useEffect的函数就会再次执行,
首次list是空数组是复杂数据类型, 发请求得到结果更改了list, 说明list变了,
两个数组不会相等, [] == [] // false {} == {} false 1 == 1 true
复杂数据类型比较地址,
所以永远认为list变了继续发请求...

如果非要打印list那么放到外面去打印
useEffect(() => {
const getList = async () => {
const res = await axios.get("http://geek.itheima.net/v1_0/channels");
setList(res.data.data.channels);
// console.log(res)
};
getList();
}, []);
console.log(list) //这个list会打印两次, 而且首次打印的是个空数组, 因为初始化的空数组
或者可以选择打印内部变量res

useEffect取消请求的说明

有时请求过快, 导致请求未完成就切换到其他路由或其他组件, 导致报错
所以需要在组件销毁的时候把还没完成的请求取消掉

类组件中如何取消

并不是真正取消, 因为请求已经发出了, 而是做到让请求回来后不去调用setState()

见文 love85g.com/?p=1593

hooks中如何取消

清理副作用


// 代码演示
useEffect(() => {
let isCancel = false
const getList = async () => {
const res = await axios.get("xxx");
if (isCancel) return
setList(res);
};
getList();
return () => {
// 取消请求
isCancel = true
}
}, []);

// 正常isCancel都是false, if (isCancel) return这句不会执行
// 所以发请求后setList(), 数据发生变化
// 如果这个请求还没回来就跳别的组件了,
// 那么组件就要销毁, 销毁要清理副作用走return () => { isCancel = true }
// isCancel变为true, 过一会请求回来了 const res = await axios.get("xxx"); 得到了res
// 在if (isCancel) return处就不会往下走setList了

Dan的博客 解答一切关于useEffect的疑问

https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/