Table of Contents

1、概述

useEffect 是 React 中,最难理解的 hook,在学习时,我们应该多花一点心思

在 React 中,useEffect 是用于处理副作用的 hook,它会在组件渲染后执行,并且会在组件卸载时执行

他的基础语法如下所示

引入 useEffect

index.tsx
1
import { useEffect } from 'react';

在函数组件中使用 useEffect

index.tsx
01
function App() {
02
useEffect(() => {
03
console.log('useEffect');
04
}, []);
05
06
return (
07
<div>
08
<h1>Hello, World!</h1>
09
</div>
10
);
11
}

useEffect 接收两个参数

index.tsx
1
useEffect(effect, deps);
  • effect:回调函数,在组件渲染后执行
  • deps:依赖项,当依赖项发生变化时,effect 函数会重新执行, 如果 deps 为空数组,则 effect 函数会在组件渲染后执行一次; 如果 deps 不为空数组,则 effect 函数会在组件渲染后执行,并且会在依赖项发生变化时重新执行

3、什么是副作用

在 React 中,由 state 的变化导致 UI 发生变化的过程是正常操作,其他操作行为:例如数据请求、直接手动修改 DOM 节点、直接操作页面,修改页面标题等、记录日志等都是副作用操作。

副作用操作是相对于操作 state 而言的

每一次因为 state 的改变,都有一次对应副作用函数的执行时机。如果 state 多次改变,那么就有多次对应副作用的执行时机

如下案例中,点击事件触发了 state 的改变,然后导致 UI render,从而导致副作用 effect 函数的执行

index.tsx
01
import { useState, useEffect } from 'react';
02
03
function Example() {
04
const [count, setCount] = useState(0);
05
06
useEffect(() => {
07
// Update the document title using the browser API
08
document.title = `You clicked ${count} times`;
09
});
10
11
return (
12
<div>
13
<p>You clicked {count} times</p>
14
<button onClick={() => setCount(count + 1)}>
15
Click me
16
</button>
17
</div>
18
);
19
}
预览
React Execution Flow

useEffect 执行时机演示

/01
count: 0

State Change

Trigger Update
/02

UI Render

DOM Paint
/03

Side Effect

effect()

4、实践:数据请求

在 React 18 中以及之前的版本中,我们通常都会使用 useEffect 来实现数据请求. 这是它最常用的场景,核心步骤是

  1. 将数据定义在 useState 中
  2. 在 useEffect 中请求数据
  3. 请求成功后,通过 setState 更新数据,JSX 会自动更新
预览
index.tsx
api.ts
message.tsx
01
import React, { useEffect, useState } from 'react'
02
import Skeleton from 'components/zmui/skeleton'
03
import Message from './message'
04
import { getMessage } from './api'
05
06
export default function Demo04() {
07
const [content, update] = useState({ value: '' })
08
const [loading, setLoading] = useState(true)
09
10
useEffect(() => {
11
getMessage().then(res => {
12
update(res)
13
setLoading(false)
14
})
15
}, []);
16
17
return (
18
<div className='p-4'>
19
{loading ? <Skeleton type='header' /> : <Message message={content.value} />}
20
</div>
21
)
22
}
23

此时,由于 useEffect 的依赖项为空数组,因此,effect 函数仅在组件初始化时执行一次

INFO

注意:在开发环境时,React 严格模式下,即使你传入的依赖项为空数组,也会执行两次 effect 函数,他会主动将组件卸载并再次挂载。这是为了帮助你发现潜在的问题,以确保清理函数的正确。在生产环境时,不会执行两次

我们可以在依赖项数组中,放入 loading 状态,当我们想要更新请求时,只需要更新 loading 状态就可以让 effect 函数再次执行

当然,需要加一个判断,当 loading 状态为 true 时,才请求数据

预览
index.tsx
api.ts
message.tsx
01
import { useEffect, useState } from 'react'
02
import Skeleton from 'components/zmui/skeleton'
03
import Message from './message'
04
import { getMessage } from './api'
05
06
export default function Demo04() {
07
const [content, update] = useState({ value: '' })
08
const [loading, setLoading] = useState(true)
09
10
useEffect(() => {
11
if (!loading) return;
12
getMessage().then(res => {
13
update(res)
14
setLoading(false)
15
})
16
}, [loading]);
17
18
return (
19
<div className='p-4'>
20
<div className='text-right mb-4'>
21
<button className='button' onClick={() => setLoading(!loading)}>更新数据</button>
22
</div>
23
{loading ? <Skeleton type='header' /> : <Message message={content.value} />}
24
</div>
25
)
26
}
27

5、useEffect 的清理函数

有的时候,副作用函数 effect 执行会留下一些痕迹,因此 useEffect 提供了一种清除副作用的方式,我们称之为 cleanup effect

effect 与 cleanup effect 是一一对应的紧密关系。在语法表现上,我们定义一个回调函数由 effect 执行时返回,该函数就是 cleanup effect 函数

index.tsx
1
useEffect(() => {
2
// dosomething
3
4
// 定义 cleanup effect 函数
5
return () => {
6
// clear something
7
}
8
}, [])

它的具体执行实际如下所示

预览
React Lifecycle Visualization

Render Cleanup Effect

STEP 01
0
Update State
Trigger Re-render
STEP 02
UI Render
Paint to DOM
STEP 03
Cleanup Effect
Run prev return()
STEP 04
Run Effect
Run new effect()
等待交互...

通常,我们使用 cleanup 函数来取消定时器、取消订阅、取消网络请求等操作

6、使用 cleanup effect 实现防抖

由于 useEffect 的执行时机与 state 的变化密切相关,因此,我们可以利用 cleanup effect 来实现防抖效果,核心步骤如下

  1. 分别定义两个状态,一个正常更新,一个用于防抖更新
  2. 在 useEffect 中,设定定时器,在定时器到期后,更新防抖状态
  3. 在 cleanup effect 中,清除定时器

连续点击按钮,观察 2 个状态的变化

WARNING

JSX 部分大多数内容是为了案例的演示样式更好看,所以代码比较繁杂,学习时,你可以仅关注逻辑部分,直接跳过他们

预览
State.立即更新
00
Updated: Instantly
State.防抖更新
00
Status: SYNCED
Logic: 当你在 600ms 内连续点击,useEffect cleanup 会清除上一次的定时器,阻止右侧数值更新。只有停止操作后,定时器才能走完。
index.tsx
01
import { useState, useEffect } from 'react';
02
03
export default function debouncedCount() {
04
const [count, setCount] = useState(0);
05
const [debouncedCount, setDebouncedCount] = useState(0);
06
const [isWaiting, setIsWaiting] = useState(false);
07
08
// 核心防抖逻辑
09
useEffect(() => {
10
// 只有当 count 变化时,进入 Effect
11
setIsWaiting(true);
12
13
// 设定定时器
14
const timer = setTimeout(() => {
15
setDebouncedCount(count);
16
setIsWaiting(false);
17
}, 600); // 600ms 防抖时间
18
19
// Cleanup 函数:如果在 600ms 内 count 再次变化,
20
// React 会先执行这个 cleanup 清除上一个定时器,
21
// 从而实现了“防抖”效果
22
return () => {
23
clearTimeout(timer);
24
};
25
}, [count]);
26
27
const handleClick = () => {
28
setCount((c) => c + 1);
29
};
30
31
return (
32
<div className="w-full p-8 bg-[#050505] text-neutral-200 flex flex-col items-center justify-center font-sans selection:bg-white selection:text-black">
33
<div className="relative z-10 w-full max-w-3xl px-6">
34
<div className="grid grid-cols-1 md:grid-cols-2 gap-px bg-neutral-800 border border-neutral-800">
35
{/* 左侧:实时响应 */}
36
<div className="bg-[#0a0a0a] p-10 flex flex-col justify-between h-64 relative group overflow-hidden">
37
<div className="flex justify-between items-start">
38
<span className="text-xs font-mono text-neutral-500 uppercase tracking-widest">State.立即更新</span>
39
<div className="w-2 h-2 bg-blue-500 animate-pulse"></div>
40
</div>
41
42
<div className="font-mono text-7xl text-white font-light tracking-tighter">
43
{count.toString().padStart(2, '0')}
44
</div>
45
46
<div className="text-xs text-neutral-400 font-mono">Updated: <span className="text-white">Instantly</span></div>
47
</div>
48
49
{/* 右侧:防抖响应 */}
50
<div className="bg-[#0a0a0a] p-10 flex flex-col justify-between h-64 relative overflow-hidden">
51
<div className="flex justify-between items-start">
52
<span className="text-xs font-mono text-neutral-500 uppercase tracking-widest">State.防抖更新</span>
53
<div className={`w-2 h-2 transition-colors duration-200 ${isWaiting ? 'bg-amber-500' : 'bg-emerald-500'}`}></div>
54
</div>
55
56
<div className={`font-mono text-7xl font-light tracking-tighter transition-colors duration-300 ${isWaiting ? 'text-neutral-600' : 'text-amber-500'}`}>
57
{debouncedCount.toString().padStart(2, '0')}
58
</div>
59
60
<div className="text-xs text-neutral-400 font-mono flex items-center gap-2">
61
Status: {isWaiting ? <span className="text-amber-500 animate-pulse">WAITING (CLEANUP PENDING)</span> : <span className="text-emerald-500">SYNCED</span>}
62
</div>
63
</div>
64
</div>
65
66
{/* 控制区 */}
67
<div className="mt-px grid grid-cols-1 xl:grid-cols-[1fr_auto] gap-px bg-neutral-800 border border-t-0 border-neutral-800">
68
{/* 说明文字 */}
69
<div className="bg-[#0a0a0a] p-6 flex items-center">
70
<div className="text-xs text-neutral-300 font-mono leading-relaxed">
71
<span className="text-white">Logic:</span> 当你在 600ms 内连续点击,<span className="text-amber-500">useEffect cleanup</span> 会清除上一次的定时器,阻止右侧数值更新。只有停止操作后,定时器才能走完。
72
</div>
73
</div>
74
75
{/* 交互按钮 */}
76
<button onClick={handleClick} className="relative bg-white text-black px-12 py-6 hover:bg-amber-500 hover:text-black active:bg-amber-600 transition-colors duration-150 font-bold text-sm tracking-[0.2em] uppercase flex items-center justify-center gap-3 group">
77
<span>快速点击 / 连续点击</span>
78
</button>
79
</div>
80
</div>
81
</div>
82
);
83
}

6、如何避免 useEffect 在开发环境中执行两次

通常情况下,我们不应该避免 useEffect 初始化时在开发环境中执行两次。因为这个机制存在的原因是为了帮助我们发现潜在的问题

但是在一些场景之下,你确实需要保持开发环境与生产环境的一致性,那么,我们可以通过取消严格模式来实现

在 react 中

index.tsx
01
// 修改之前
02
ReactDOM.createRoot(document.getElementById('root')).render(
03
<React.StrictMode>
04
<App />
05
</React.StrictMode>
06
)
07
08
// 修改之后
09
ReactDOM.createRoot(document.getElementById('root')).render(
10
<App />
11
)

next.js

next.config.ts
1
/** @type {import('next').NextConfig} */
2
const nextConfig = {
3
reactStrictMode: false, // 将其设置为 false
4
};
5
6
module.exports = nextConfig;

当然,你也可以通过创建一个 ref 标记,来避免内部逻辑中执行两次

index.tsx
01
import { useEffect, useRef } from 'react';
02
03
function MyComponent() {
04
const hasRun = useRef(false); // 创建一个标记
05
06
useEffect(() => {
07
// 如果已经执行过,直接返回
08
if (hasRun.current) return;
09
10
// 标记为已执行
11
hasRun.current = true;
12
13
// --- 这里写你的业务逻辑 ---
14
console.log('这就只会执行一次了');
15
fetch('/api/data').then(...);
16
17
}, []); // 空数组
18
19
return <div>Hello World</div>;
20
}

2、总结

如果只是入门级理解,那么阅读本文就行,如果要彻底透彻的理解,你需要结合闭包、事件循环的底层基础知识,你可以在下面两个专栏中深入学习

  1. JS 核心进阶
  2. React 底层原理

在后续的章节中,我们还需要学习更多关于 useEffect 的知识

专栏首页
到顶
专栏目录