文章目录
- React 的 useEffect
- 一、什么是副作用(Side Effects)?
- 二、useEffect 的基本用法
- 三、依赖数组的三种情况
- 1. 无依赖数组(每次渲染后都执行, 不推荐)
- 2. 空依赖数组(仅在挂载时执行一次)
- 3. 有依赖项(依赖变化时执行)
- 四、常见应用场景
- 1. 数据请求:根据参数动态加载数据
- 2. 事件监听:窗口大小变化时更新状态
- 3. 定时器:倒计时功能
- 4. 动画帧(requestAnimationFrame)
- 5. 本地存储同步
- 五、清理函数(Cleanup Function)
- 六、性能优化与注意事项
- 1. 避免无限循环
- 2. 依赖项是对象或数组时
- 3. 按职责拆分副作用
- 七、常见问题与解决方案
- 1. 如何在 useEffect 中使用异步函数?
- 2. 依赖项缺失导致逻辑错误
- 八、总结与最佳实践
React 的 useEffect
useEffect
是 React Hooks 中最重要的 API 之一,用于处理组件中的副作用(Side Effects),例如数据请求、DOM 操作、订阅事件等。本文将从基础用法、核心原理、常见问题到最佳实践,全面解析 useEffect
的使用技巧。
一、什么是副作用(Side Effects)?
在 React 中,副作用是指那些与组件渲染结果无直接关系,但可能影响其他组件或外部系统的操作。例如:
- 数据请求(API 调用)
- 手动修改 DOM
- 订阅事件(如 WebSocket、键盘事件)
- 设置定时器
类组件中,副作用通常写在生命周期方法(如 componentDidMount、componentDidUpdate)中。而函数组件通过 useEffect 统一管理副作用。
二、useEffect 的基本用法
useEffect(() => {// 副作用逻辑return () => {/* 清理函数(可选) */}
}, [dependencies])
- 第一个参数:一个包含副作用逻辑的函数(必填)
- 第二个参数:依赖数组(可选),用于控制副作用的执行时机。
- 返回值:清理函数(可选),用于在组件卸载或下次副作用执行前释放资源。
三、依赖数组的三种情况
1. 无依赖数组(每次渲染后都执行, 不推荐)
useEffect(() => {console.log('每次组件更新后执行')
})
- 行为 :组件每次渲染(包括首次渲染和更新)后都会执行。
- 风险 :可能导致性能问题或无限循环(如在副作用中修改状态)。
2. 空依赖数组(仅在挂载时执行一次)
useEffect(() => {console.log('仅在挂载时执行一次')
})
- 行为 :仅在组件首次渲染后执行一次,类似类组件的 componentDidMount。
- 用途 :初始化操作(如请求初始数据、订阅事件)。
3. 有依赖项(依赖变化时执行)
useEffect(() => {console.log('当 count 变化时执行')
}, [count])
- 行为 :首次渲染后执行,后续仅在依赖项 count 变化时执行。
- 关键点 :依赖项必须是基本类型(如数字、字符串)或稳定引用(通过 useMemo/useCallback 包裹的复杂类型)。
四、常见应用场景
1. 数据请求:根据参数动态加载数据
import { useState, useEffect } from 'react'
import axios from 'axios'function UserProfile({ userId }) {const [userData, setUserData] = useState(null)const [loading, setLoading] = useState(false)useEffect(() => {// 定义取消请求的标记let isCancelled = falseconst fetchUserData = async () => {try {setLoading(true)const response = await axios.get(`/api/users/${userId}`)// 仅在组件未卸载时更新状态if (!isCancelled) {setUserData(response.data)setLoading(false)}} catch (error) {if (!isCancelled) {setLoading(false)console.error('Fetch error:', error)}}}fetchUserData()// 清理函数:取消未完成的请求return () => {isCancelled = true}}, [userId]) // 当 userId 变化时重新加载数据return (<div>{loading ? 'Loading...' : userData && <div>{userData.name}</div>}</div>)
}
2. 事件监听:窗口大小变化时更新状态
import { useState, useEffect } from 'react'function WindowSizeTracker() {const [windowSize, setWindowSize] = useState({width: window.innerWidth,height: window.innerHeight})useEffect(() => {// 定义事件处理函数const handleResize = () => {setWindowSize({width: window.innerWidth,height: window.innerHeight})}// 添加监听window.addEventListener('resize', handleResize)// 清理函数:移除监听return () => {window.removeEventListener('resize', handleResize)}}, []) // 空依赖数组:仅挂载时添加一次监听return (<div>Window Size: {windowSize.width}px x {windowSize.height}px</div>)
}
3. 定时器:倒计时功能
import { useState, useEffect } from 'react'function CountdownTimer({ initialSeconds }) {const [seconds, setSeconds] = useState(initialSeconds)useEffect(() => {// 定义定时器const timer = setInterval(() => {setSeconds((prev) => {if (prev <= 1) {clearInterval(timer) // 倒计时结束清除定时器return 0}return prev - 1})}, 1000)// 清理函数:组件卸载时清除定时器return () => clearInterval(timer)}, []) // 空依赖数组:只在挂载时启动定时器return <div>Time Left: {seconds} seconds</div>
}
4. 动画帧(requestAnimationFrame)
import { useState, useEffect, useRef } from 'react'function AnimationBox() {const [position, setPosition] = useState(0)const requestRef = useRef() // 保存动画帧 IDconst animate = () => {setPosition((prev) => (prev >= 100 ? 0 : prev + 1))requestRef.current = requestAnimationFrame(animate) // 递归调用}useEffect(() => {requestRef.current = requestAnimationFrame(animate)// 清理函数:取消动画帧return () => cancelAnimationFrame(requestRef.current)}, []) // 空依赖数组:只在挂载时启动动画return (<div style={{ transform: `translateX(${position}px)` }}>Moving Box</div>)
}
5. 本地存储同步
import { useState, useEffect } from 'react'function ThemeSwitcher() {const [theme, setTheme] = useState(() => {// 从 localStorage 读取初始值const savedTheme = localStorage.getItem('theme')return savedTheme || 'light'})useEffect(() => {// 当 theme 变化时,同步到 localStoragelocalStorage.setItem('theme', theme)}, [theme]) // 依赖 theme,变化时触发return (<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Switch to {theme === 'light' ? 'Dark' : 'Light'} Theme</button>)
}
五、清理函数(Cleanup Function)
清理函数在以下时机执行:
- 组件卸载时(类似 componentWillUnmount)。
- 下次副作用执行前(依赖项变化时)。
示例:取消订阅
useEffect(() => {const subscription = eventEmitter.subscribe(() => {/* ... */})return () => subscription.unsubscribe()
}, [])
六、性能优化与注意事项
1. 避免无限循环
在副作用中直接修改依赖项会导致无限循环:
// ❌ 错误:每次更新后修改 count,触发重新渲染
useEffect(() => {setCount(count + 1)
}, [count])
2. 依赖项是对象或数组时
如果依赖项是对象或数组,即使内容相同,引用变化也会触发副作用:
// ❌ 可能意外触发
const config = { enabled: true };
useEffect(() => { ... }, [config]);// ✅ 用 useMemo 稳定引用
const config = useMemo(() => ({ enabled: true }), []);
3. 按职责拆分副作用
// 拆分数据请求和事件监听
useEffect(() => {/* 请求数据 */
}, [])
useEffect(() => {/* 监听事件 */
}, [])
七、常见问题与解决方案
1. 如何在 useEffect 中使用异步函数?
不能直接将 useEffect 的回调设为 async,但可以在内部定义异步函数:
useEffect(() => {const fetchData = async () => {const result = await axios.get(url)setData(result)}fetchData()
}, [url])
2. 依赖项缺失导致逻辑错误
启用 eslint-plugin-react-hooks 规则,确保依赖项完整。
八、总结与最佳实践
- 明确依赖项 :始终填写依赖数组,避免遗漏。
- 拆分副作用 :不同逻辑使用多个 useEffect。
- 及时清理资源 :防止内存泄漏。
- 稳定引用 :使用 useCallback 和 useMemo 处理复杂依赖。
通过合理使用 useEffect,可以写出更清晰、健壮的 React 组件。