← 全部笔记React Hooks · 6 min
FIELD NOTE / 012026.08.06

useEffect:从参数理解到项目实践

这篇笔记按照三个层次理解 useEffect:先看 React 官网的定义和参数,再用一个最小案例观察依赖数组,最后回到项目里的 ProTable 列表查询场景。

一、useEffect 是什么

React 官方把 useEffect 定义为:用于让组件和外部系统保持同步的 Hook。

外部系统可以是网络请求、浏览器 API、订阅、定时器,也可以是一个非 React 的第三方组件。它的基本写法是:

tsxFIELD NOTE
useEffect(setup, dependencies?)

这里的重点不是“组件加载后执行一段代码”,而是:当组件中的某些响应式值发生变化时,重新同步外部系统。

官方文档:`useEffect` Reference

如果一段逻辑并不是在同步外部系统,而只是根据已有数据计算另一个值,通常不需要使用 useEffect。

二、两个参数分别做什么

1. setup:Effect 的主体逻辑

第一个参数是一个函数,用来放置需要同步的逻辑。它可以返回一个清理函数:

tsxFIELD NOTE
useEffect(() => {  // setup:建立连接、绑定事件或发起同步   return () => {    // cleanup:断开连接、取消订阅或清理资源  };}, [dependency]);

React 会按照这样的顺序执行:

初次提交之后

组件完成渲染并提交到页面后,执行 setup。

依赖发生变化之后

先用旧值执行上一次的 cleanup,再用新值执行 setup。

组件卸载之后

最后执行一次 cleanup。

如果 Effect 只是调用一次查询方法,没有需要持续占用的连接、事件或定时器,就不一定需要返回清理函数。

2. dependencies:Effect 依赖的响应式值

第二个参数是依赖数组,应该写入 setup 中使用到的响应式值。响应式值包括:

React 会使用 Object.is 比较依赖的新旧值。依赖数组的长度应该固定,并且直接写在代码里,例如 [roomId, serverUrl]。

不同写法代表不同的触发时机:

不传依赖数组

tsxFIELD NOTE
useEffect(() => {  // 每次组件提交后都执行});

传空数组 []

tsxFIELD NOTE
useEffect(() => {  // 初次提交后执行}, []);

它表示这个 Effect 没有依赖组件中的响应式值。不过开发环境开启 Strict Mode 时,React 可能额外执行一次“setup → cleanup → setup”,用来检查清理逻辑是否完整。

传入具体依赖

tsxFIELD NOTE
useEffect(() => {  // count 变化后重新执行  document.title = `Count: ${count}`;}, [count]);

这里 setup 使用了 count,所以 count 必须出现在依赖数组中。依赖数组不是用来“挑选想监听的变量”,而是要准确描述 Effect 读取了哪些响应式值。

三、一个简单案例:同步页面标题

下面的组件把计数器的值同步到浏览器标签页标题:

tsxFIELD NOTE
import { useEffect, useState } from "react"; export default function Counter() {  const [count, setCount] = useState(0);   useEffect(() => {    document.title = `当前数量:${count}`;  }, [count]);   return (    <button onClick={() => setCount((value) => value + 1)}>      点击次数:{count}    </button>  );}

这个案例的执行过程

第一次渲染

count 是 0。组件提交到页面后,Effect 把标题设置为“当前数量:0”。

点击按钮

setCount 让 count 变成 1。组件重新渲染并提交后,React 发现 [count] 中的值发生变化,于是再次执行 Effect,把标题更新为“当前数量:1”。

点击其他不相关的按钮

如果组件重新渲染,但 count 的值没有变化,React 不会因为这次渲染重新执行这个 Effect。

这个案例可以帮助我们建立一个简单的判断方式:

setup 里读了谁,就把谁写进依赖数组;依赖变了,就重新同步外部系统。

四、回到项目:等待枚举准备好再查询

项目里的实际场景是一个 ProTable 列表页。页面查询依赖一些枚举值,例如状态和启用状态。第一次渲染时,这些枚举可能还没有准备完成,因此不能立即把未完成的查询条件提交给后端。

核心代码是:

tsxFIELD NOTE
useEffect(() => {  if (isAll || !pendingEnumReady) return;  instance.commitSearch?.();}, [instance, isAll, pendingEnumReady]);

1. setup 做了什么

这里的 setup 逻辑很简单:调用 instance.commitSearch?.(),触发 ProTable 提交查询并请求列表数据。

它没有建立长连接、绑定事件或启动定时器,所以不需要额外的 cleanup。

2. pendingEnumReady 为什么是依赖

第一次渲染时,如果 pendingEnumReady 是 false,代码直接返回,不发起查询。

当枚举加载完成,pendingEnumReady 变成 true,React 发现依赖发生变化,再执行一次 Effect。这时查询条件已经完整,才触发 commitSearch。

这就是“先准备依赖,再执行副作用”的实际应用。

3. isAll 为什么要提前返回

全量列表已经通过 autoFetch={isAll} 自动查询。当 isAll 为 true 时,如果 Effect 再调用一次 commitSearch,就可能造成重复请求。

所以这句代码:

tsxFIELD NOTE
if (isAll || !pendingEnumReady) return;

表达了两个业务保护条件:

4. 依赖数组如何和项目逻辑对应

Effect 的代码读取了三个外部值:

因此依赖数组写成 [instance, isAll, pendingEnumReady]。这不是为了让 Effect“只在某个变量变化时执行”,而是如实声明这段逻辑依赖了哪些响应式值。

5. 完整的执行时序

可以把这段代码想成四步:

页面第一次渲染

枚举还没加载完成,pendingEnumReady 为 false,Effect 提前结束,不发送错误查询。

枚举加载完成

pendingEnumReady 变成 true,Effect 重新执行。

判断列表类型

如果 isAll 为 true,交给 autoFetch 处理;否则继续执行手动查询逻辑。

提交查询

调用 instance.commitSearch?.(),让 ProTable 使用完整条件请求列表。

一句话总结:这个 Effect 不是为了“页面打开后强行请求一次”,而是为了在查询依赖准备完成之后,把列表状态同步给 ProTable,并通过条件判断避免重复请求和不完整参数。

五、Review 这类 Effect 时看什么

先确认它是否真的需要 Effect

如果只是计算一个值,可以直接在渲染阶段计算,或者使用 useMemo;如果是同步网络、浏览器 API、订阅或第三方组件,才考虑 Effect。

检查依赖是否完整

不要为了让 Effect 少执行而随意删除依赖,也不要用 eslint 注释压制 exhaustive-deps。如果依赖不完整,Effect 可能读到旧的 props 或 state。

检查是否会重复请求

看自动请求和手动 commitSearch 是否同时存在,确认是否需要像 isAll 这样的保护条件。

检查条件是否准备完成

如果请求参数来自异步枚举、用户信息或路由参数,要确认 Effect 不会在这些值尚未准备好时提前执行。

如果建立了资源,必须清理

订阅、事件监听、定时器、连接和可取消请求,都应该在 cleanup 中撤销或关闭,让 setup 和 cleanup 成对出现。

判断一个 Effect 是否可靠,可以问自己:如果 React 先执行一次 setup,再立即 cleanup,最后再次 setup,页面和外部系统是否仍然正确?