1. 使用 useEffect 的核心作用与生命周期联动
1.1 useEffect 的定义与副作用
useEffect 是 React 函数组件中的副作用钩子,用于在渲染之后执行带有副作用的操作,例如数据获取、DOM 操作、事件订阅等。通过它,组件能够在恰当的时机接收外部影响并完成相应的处理。
副作用的本质是与渲染结果分离的行为,这使得 UI 与数据源的变化解耦,提升了组件的可维护性。使用 useEffect 可以把副作用集中在一个地方管理,从而避免直接在渲染函数中执行复杂逻辑。
在初次渲染之后执行、或在依赖项变化时再执行,这是 useEffect 的核心行为。为了实现这一点,需要把副作用放在回调函数中,并通过依赖项数组控制执行时机。
import React, { useEffect, useState } from 'react';function Example({ userId }) {useEffect(() => {// 需要在渲染后执行的副作用fetch(`/api/user/${userId}`).then(res => res.json()).then(data => {console.log(data);});}, [userId]); // 依赖项数组:userId 变化时重新执行return 查看控制台输出;
}
1.2 依赖项配置对执行时机的影响
依赖项数组决定副作用的触发时机,空数组表示仅在首次渲染后执行一次;包含具体变量则在这些变量变化时重新执行;不提供数组则会在每次渲染后执行。
正确声明依赖项可以避免不必要的重执行,从而提升性能与响应速度。错误的依赖声明可能导致未预期的更新,甚至出现内存泄漏的风险。
处理对象、函数等不可比对的依赖时要格外小心,因为对象或函数的引用在每次渲染时都可能变化,需借助 useMemo 或将稳定的回调传入依赖项数组。
1.3 清理函数与内存管理
清理函数用于移除订阅、取消计时器、释放资源等操作,确保组件卸载或再次触发副作用前释放先前创建的资源。
清理函数在下一次副作用执行前执行,并且在组件卸载时也会执行,帮助防止内存泄漏与重复注册事件。
在有全局事件监听或外部订阅时尤其重要,如监听窗口事件、WebSocket 连接、定时器等,需要在清理阶段断开连接或移除监听器。
import React, { useEffect } from 'react';function ResizeWatcher() {useEffect(() => {const onResize = () => console.log('宽度变化');window.addEventListener('resize', onResize);return () => {window.removeEventListener('resize', onResize);};}, []); // 仅在挂载和卸载时添加/移除监听return <div>监听窗口宽度</div>;
}
2. useEffect 的常见使用场景
2.1 数据获取与 API 调用
数据获取通常放在 useEffect 内部,以确保在组件渲染后发起网络请求并处理结果。
通过依赖项控制请求触发时机,例如在用户输入或筛选条件改变时重新拉取数据,避免不必要的请求。
请求完成后要更新状态以触发重新渲染,并在组件卸载时进行取消(如取消未完成的请求)以避免内存泄漏。
import React, { useEffect, useState } from 'react';function UserList({ query }) {const [users, setUsers] = useState([]);useEffect(() => {let mounted = true;fetch(`/api/users?q=${encodeURIComponent(query)}`).then(res => res.json()).then(data => {if (mounted) setUsers(data);});return () => { mounted = false; };}, [query]);return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
2.2 订阅/事件监听
订阅外部事件源(如订阅消息或自定义事件总线)是常见场景,需要在 useEffect 中建立订阅并在清理阶段退订。
订阅的取消要与创建订阅的阶段对称,以防止未处理的回调在组件已卸载时执行,导致内存泄漏或错误。
import React, { useEffect } from 'react';function MessageSubscriber({ onMessage }) {useEffect(() => {const handler = (msg) => onMessage(msg);window.addEventListener('message', handler);return () => window.removeEventListener('message', handler);}, [onMessage]);return <div>消息订阅中</div>;
}
2.3 与第三方库的初始化与清理
很多第三方库需要在组件挂载时初始化,在卸载时清理,例如图表库、地图库或滑动组件。
初始化通常放在副作用中完成,确保浏览器环境就绪,并且仅在需要时创建实例,减少对性能的影响。
import React, { useEffect } from 'react';
import Chart from 'chart.js/auto';function LineChart({ data }) {useEffect(() => {const chart = new Chart(document.getElementById('canvas'), { type: 'line', data });return () => chart.destroy();}, [data]);return <canvas id="canvas"></canvas>;
}
3. 使用 useEffect 的常见坑与技巧
3.1 避免不必要的副作用
尽量把副作用限定在真实需要的场景,避免把 UI 渲染本身的逻辑放入副作用,以免导致复杂度上升。
用依赖项数组精准控制触发条件,尽量不要让副作用对每次渲染都执行,除非确实需要。
如果副作用与渲染强相关,考虑将相关逻辑提炼成独立的自定义钩子,提升可读性与复用性。
3.2 如何处理异步逻辑与竞态
在 useEffect 中处理异步逻辑时要注意取消未完成的请求,避免在组件已卸载后继续处理结果。

使用变量标记、AbortController 或以 mounted 标志位管理状态,以防止状态更新在卸载后进行,导致内存泄漏和错误。
useEffect(() => {const controller = new AbortController();fetch('/api/data', { signal: controller.signal }).then(res => res.json()).then(setData).catch(err => {if (err.name !== 'AbortError') console.error(err);});return () => controller.abort();
}, []);3.3 依赖项的正确声明与依赖数组的注意点
尽量把“不可变”的依赖放入依赖项数组,如 primitive 值、稳定引用的对象、场景中确实需要变化的变量。
对于函数或对象等经常重新创建的依赖,可以使用 useCallback/useMemo 固定引用,避免不必要的副作用重执行。
避免在依赖项中直接放置非必要的副作用相关状态,以免引起难以追踪的循环执行。
4. 具体代码示例:从依赖项配置到清理函数
4.1 基础示例:数据获取
基础数据获取示例是理解 useEffect 的入门场景,展示如何在渲染完成后发起网络请求并更新状态。
确保依赖项为空数组以防止重复请求,只有在组件首次渲染时执行请求。
import React, { useEffect, useState } from 'react';function Posts() {const [posts, setPosts] = useState([]);useEffect(() => {fetch('/api/posts').then(res => res.json()).then(setPosts);}, []); // 仅在挂载时执行return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
4.2 带依赖项的示例
当某些输入变化时触发副作用,例如筛选条件或用户输入,应该把它们作为依赖项。
避免在没有依赖项时触发重复请求,通过把变量放入依赖项数组实现精确控制。
import React, { useEffect, useState } from 'react';function FilteredPosts({ keyword }) {const [list, setList] = useState([]);useEffect(() => {let active = true;fetch(`/api/posts?kw=${encodeURIComponent(keyword)}`).then(res => res.json()).then(data => { if (active) setList(data); });return () => { active = false; };}, [keyword]);return <ul>{list.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
4.3 取消订阅与清理
在组件卸载时清理订阅或取消请求,以防止回调在不可用的组件上执行。
通过返回清理函数实现对副作用的对称释放,确保资源在生命周期内得到正确管理。
import React, { useEffect } from 'react';function Clock() {useEffect(() => {const id = setInterval(() => console.log('tick'), 1000);return () => clearInterval(id);}, []);return <div>时钟</div>;
}


