React

Effect Hooks - useEffect

useEffect用于在函数组件中执行副作用操作,如数据获取、订阅或手动更改 DOM
This entry is part 2 of the seriesenjoy-react-hooks

Side Effect

在编程中,Side Effect(副作用)是一种描述函数或操作对其环境产生影响的术语。在纯函数式编程中,函数不应该有副作用,即它们只应根据输入值计算输出,而不修改任何外部状态。但在实际应用中,特别是在UI框架如React中,副作用是不可避免的,因为我们经常需要与浏览器环境交互,比如:

  • 数据获取:从服务器获取数据。
  • 订阅:订阅某些事件源,如WebSocket连接、Redux store的变化等。
  • 手动修改DOM:直接操作DOM元素。
  • 日志记录:记录应用程序的运行信息。
  • 浏览器缓存:使用localStorage或sessionStorage进行数据存储。
  • 定时任务:设置setTimeout或setInterval。

useEffect语法

useEffect语法如下:

  • setup: 处理 Effect 的函数
    • cleanup?: setup 函数选择性返回一个 清理(cleanup) 函数
  • dependencies?: 依赖项数组(可选)

useEffect用法

不传递依赖项数组

如果不传递依赖项数组,则每次Rnder后都会执行effect

传递空的依赖项数组

当传递一个空的依赖数组时,effect 将只在Mount阶段执行一次,后续Render不再执行。

传递非空的依赖项数组

当传递一个非空的依赖数组时,仅在依赖项发生变动时执行effect

带cleanup函数

在上述所有示例中,我们都没有使用可选的cleanup,但在某些情况下,我们可能需要使用清理函数

假设我们有个需求:

  1. 开关打开时,监听鼠标移动
  2. 开关关闭时,不监听鼠标移动

拿到需求后我开发了第一个版本,但是我发现我只实现了需求1,当开关关闭时,页面仍在监听鼠标移动

仔细分析上面代码,能发现两个问题

  1. 开关关闭(tracking为false)后,因为上一次开关打开时注册的事件仍然存在,导致页面仍在打印鼠标的座标信息
  2. 开关再次打开时会重复注册mousemove事件

而使用cleanup是能够解决上面两个问题的

我们来分析下上面代码的执行逻辑

  1. 当组件挂载(第一次Render)时,effect执行,此时我们会注册事件监听,并返回一个cleanup
  2. 随着用户移动鼠标,mousePosition状态发生改变,重新触发Render,但mousePosition并不时effect的依赖项,所以effect在这次Render中不会执行
  3. 用户点击按钮tracking状态由true变为false,又重新触发Render 且tracking是effect的依赖项,所以effect会在这次Render中执行
    1. 但在执行effect之前,React会先执行上一次effect执行时返回的cleanup(移除事件监听)
    2. cleanup执行之后,才会执行本次effect
  4. 因为此时tracking为fasle, effect具体逻辑会被if忽略
  5. 每次点击开关都会触发上述操作

具体的操作顺序如下:

useEffect注意事项

非必要不用useEffect

当你需要基于propsstate计算某些值时,不要把计算逻辑放到useEffect

相反你可以放到render阶段, 好处就是代码量少,不易引发bug,且速度更快。

避免无限循环

下面代码会造成无线循环:

  1. 首次Render后执行Effect setViews 触发Re-Render
  2. user对象重新创建,且user是Effect的依赖项目,Effect再次执行, 又会执行setViews, 再次触发Re-Render
  3. 循环往复...

解决方案是: 使用useMemo包裹user对象,这将会阻止React在Re-Render时创建新的对象引用

另外注意,对于函数可以使用useCallback去处理,用法类似

避免竞争条件

下面代码可能会出现以下现象:

  1. 第一次渲染时 userId = 1
  2. 第二次渲染时 userId = 2
  3. 第一次 fetch('/user/1') 耗时可能远远高于第二次 fetch('/user/2')
  4. 导致最终显示的是 userId为1的用户数据

解决方案:使用AbortController来取消旧的请求

避免执行父组件传递的回调函数

比如:

解决方案: 在event handler中去处理

自定义hook: useEffectAfterMount()

useEffect 会在首次渲染以及后续依赖发生变动时都会执行

我们自定义的useEffectAfterMount会跳过首次渲染,仅在依赖项发生变动时执行。

Questions? Let's chat

discord logoOPEN DISCORD
6423
members online
previous article
State Hooks - useState
next article
Permormance Hooks - useMemo & useCallback

A TypeScript Full Stack Blog

Share articles about Typescript, React, Next.js, Node.js and Css from time to time.
No spam, unsubscribe at any time.