Effect
在React中被专门定义为:由渲染引起的副作用。它允许你指定由渲染本身,而不是特定事件引起的副作用
useEffect
被设计用于将组件和外部系统同步,例如数据获取、服务器的链接、设置订阅等不受React控制的系统
作为一个React Hook,它只能在组件的顶层调用,如果你需要在循环或判断的逻辑中调用,可以将这部分逻辑抽离出去成为一个组件。
语法
useEffect(setup, dependencies?)
setup
: 处理Effect的函数,可以选择性的返回一个清理(cleanup)函数。当 组件被添加到DOM时 和 每次依赖项发生变化重新渲染后,React会首先利用旧值运行cleanup
函数(如果你设置了该函数),然后使用新值运行setup
函数。在组件从DOM中移除后,React将最后一次运行cleanup
函数dependencies
(可选):setup
函数中引用的所有响应式值的列表。- React会使用
Object.is
来比较每个依赖项和它先前的值,只要有依赖项和先前的值不同时,将会重新运行setup
函数。 - 如果忽略此参数,则在每次重新渲染组件之后,将重新执行
setup
函数 - 如果此参数为空数组
[]
,则只在初次渲染组件的时候执行setup
函数
- React会使用
useEffect
的运行过程:
- 将组件挂载到页面时,将运行
setup
函数 - 重新渲染依赖项变更的组件后:
- 首先,使用旧值运行
cleanup
函数 - 然后,使用新值运行
setup
函数
- 当组件从页面卸载后,
cleanup
函数将运行最后一次
在开发环境下,React在运行
setup
之前会额外运行一次setup
和cleanup
,这是一种压力测试,来验证Effect逻辑是否正确实现
使用示例:模拟一个连接服务器的组件
1 | import { useEffect, useState } from "react"; |
useEffect
执行时机
每当你的组件渲染时,React将更新视图,然后运行useEffect
中的代码。换句话说,**useEffect
会把这段代码放到视图更新渲染之后执行**
实践导向
在Effect中根据先前的state更新state
当想在Effect中根据先前的state更新state时,会遇到问题:
1 | import { useEffect, useState } from "react"; |
因为cnt
是响应式数据,所以必须在依赖项列表中指定它,这就会导致Effect在每次cnt变化之后都要执行cleanup
和setup
解决方法:在setCnt
中不是直接传入修改的值,而是传入c => c + 1
状态更新器,这样做的目的是:将cnt从依赖项中移除
1 | const [cnt, setCnt] = useState(0) |
Effect依赖于对象或函数
如果你的Effect依赖于渲染期间创建的对象或函数,则它可能会频繁运行
示例代码:
1 | import { useEffect, useState } from "react"; |
对createOptions
函数本身的封装并没有问题。
首先,对象或函数都是引用类型的值,判断他们是否相同是通过是否指向同一块内存地址
然后,每次组件重新渲染,都会从头创建一个createOptions
函数,那这个函数的地址和之前的地址肯定不同,也就意味着这两者不是相同的(即依赖项发生了改变),会导致Effect在每次重新渲染之后再次重新执行
所以,避免使用在渲染期间创建的函数作为依赖项,请在Effect
内部声明它
1 | import { useEffect, useState } from "react"; |
通过在Effect
内部定义createOption
函数,这样Effect
只依赖于serverUrl
字符串,字符串作为基础类型值,除非你将它设置为其他值,否则它不会改变
你不需要Effect
Effect
是React范式中的一种脱围机制。它让你可以使组件和一些外部的系统同步。如果没有涉及到外部系统(例如只是像根据props或state的变化更新一个组件的state),你就不应该使用Effect
常见的情况:
- 不必使用Effect来转换渲染所需的数据。例如,想在展示一个列表之前先做筛选,你可能会写一个当列表变化时更新state的
Effect
。然而,这是低效的。
- 当你更新state,React首先会调用组件来渲染视图
- 然后React会执行你的
Effect
,如果你的Effect
也立即更新了这个state,将会重新执行整个组件
所以,你应该在组件的顶层转换数据
- 根据props或state来更新state。例如,你又一个包含了两个state变量的组件:
firstName
和lastName
。你想通过他们计算出fullName
。你可能会写一个当firstName
或者lastName
变化时更新fullName
的Effect
1 | function App() { |
这样做会导致:
- 首先,React会调用组件,使用
fullName
的旧值执行整个渲染流程 - 然后,React会执行你的
Effect
,用更新后的值又重新渲染了一遍
所以,你应该直接计算这个值const fullName = firstName + ' ' + lastName
初始化应用
有些逻辑只需要在应用加载时执行一次,你可能会将它放到一顶层组件的Effect
中
1 | function App() { |
这会遇到一些问题:在开发环境它会被执行两次,这可能会导致潜在的问题
解决方法:
添加一个顶层变量来记录它是否已经执行过了
1 | let didInit = false; |
获取数据
很多场景都需要使用Effect
来发起数据请求,例如:
1 | function SearchResults({ query }) { |
然而上面的代码有一个问题,假如你快速的输入hello
,那么query会从h
变成he
,hel
,hell
最后是hello
,这会触发一连串不同的数据请求,但无法保证返回顺序。例如,hell
的响应可能在hello
的响应之后返回,这将会显示错误的搜索结果
为了修复这个问题,需要添加一个清理函数来忽略较早的返回结果
1 | function SearchResults({ query }) { |
这可以确保在Effect
中获取数据时,除了最后一次请求的所有返回结果都将被忽略