useEffect
下面,我们来系统的梳理关于 React useEffect Hook 的基本知识点:
一、useEffect 基础概念
1.1 什么是副作用(Side Effect)?
在 React 中,副作用是指与组件渲染结果无关的操作,例如:
- 数据获取(API 请求)
- 订阅事件(WebSocket、DOM 事件)
- 手动修改 DOM
- 设置定时器
- 日志记录
- 全局状态管理
1.2 useEffect 的作用
useEffect
是 React 提供的 Hook,用于在函数组件中执行副作用操作。它取代了类组件中的生命周期方法:
componentDidMount
componentDidUpdate
componentWillUnmount
1.3 基本语法
useEffect(() => {// 副作用逻辑return () => {// 清理函数(可选)};
}, [dependencies]); // 依赖数组(可选)
二、useEffect 核心原理
2.1 执行时机
- 渲染后执行:useEffect 在浏览器完成布局与绘制之后执行
- 异步执行:不会阻塞浏览器渲染
- 顺序执行:多个 useEffect 按声明顺序依次执行
2.2 依赖数组机制
依赖数组控制 effect 的执行时机:
依赖数组 | 执行时机 |
---|---|
无依赖数组 | 每次渲染后执行 |
空数组 [] | 仅组件挂载后执行一次 |
有依赖项 [a, b] | 当 a 或 b 变化时执行 |
2.3 清理机制
三、useEffect 使用模式
3.1 无依赖数组(每次渲染后执行)
useEffect(() => {console.log('每次渲染后都会执行');
});
3.2 空依赖数组(仅挂载时执行)
useEffect(() => {console.log('仅在组件挂载后执行一次');return () => {console.log('组件卸载时执行清理');};
}, []);
3.3 有依赖数组(依赖变化时执行)
const [count, setCount] = useState(0);useEffect(() => {console.log(`count变化时执行: ${count}`);
}, [count]); // 依赖count
3.4 清理函数
useEffect(() => {const timer = setInterval(() => {console.log('定时器运行');}, 1000);return () => {clearInterval(timer); // 清理定时器};
}, []);
四、useEffect 高级用法
4.1 多个 useEffect 的使用
function UserProfile({ userId }) {const [user, setUser] = useState(null);// 获取用户数据useEffect(() => {fetchUser(userId).then(setUser);}, [userId]);// 更新文档标题useEffect(() => {document.title = user ? `${user.name}的资料` : '加载中...';}, [user]);// ...
}
4.2 在 effect 中获取最新状态
const [count, setCount] = useState(0);useEffect(() => {const timer = setInterval(() => {console.log(count); // 总是捕获初始值(闭包问题)}, 1000);return () => clearInterval(timer);
}, []);
解决方案:
// 方案1: 使用函数式更新(不适用于所有场景)
setCount(c => c + 1);// 方案2: 使用ref保存最新值
const countRef = useRef(count);
countRef.current = count;// 方案3: 添加依赖(可能触发频繁执行)
useEffect(() => {// ...
}, [count]);
4.3 异步 effect
useEffect(() => {let isMounted = true; // 防止组件卸载后设置状态const fetchData = async () => {const data = await fetch('/api/data');if (isMounted) {setData(data);}};fetchData();return () => {isMounted = false;};
}, []);
五、性能优化策略
5.1 避免不必要的 effect 执行
const [user, setUser] = useState(null);
const [profile, setProfile] = useState(null);// 不推荐:依赖整个user对象
useEffect(() => {if (user) {fetchProfile(user.id);}
}, [user]);// 推荐:只依赖必要属性
useEffect(() => {if (user?.id) {fetchProfile(user.id);}
}, [user?.id]);
5.2 使用 useCallback 优化函数依赖
const fetchData = useCallback(async () => {const data = await fetch('/api/data');setData(data);
}, []);useEffect(() => {fetchData();
}, [fetchData]);
5.3 使用 useMemo 优化对象依赖
const config = useMemo(() => ({color: theme.color,size: 'large'
}), [theme.color]);useEffect(() => {applyConfig(config);
}, [config]);
六、常见问题与解决方案
6.1 无限循环问题
问题代码:
const [count, setCount] = useState(0);useEffect(() => {setCount(count + 1); // 触发重新渲染
}, [count]); // 依赖count变化
解决方案:
- 检查是否需要更新状态
- 使用函数式更新避免依赖
useEffect(() => {setCount(c => c + 1); // 不依赖count
}, []);
6.2 依赖数组不完整
问题:ESLint 警告依赖不完整
解决方案:
- 添加所有在 effect 中使用的依赖项
- 如果确实不需要依赖变化时执行,使用
// eslint-disable-next-line
(慎用) - 重构代码避免使用外部变量
6.3 清理函数执行时机
问题:清理函数在每次依赖变化时都会执行
解决方案:
useEffect(() => {// 设置订阅const subscription = dataSource.subscribe();return () => {// 每次依赖变化时都会执行清理subscription.unsubscribe();};
}, [dataSource]); // 依赖变化触发清理
七、useEffect 最佳实践
7.1 副作用分离原则
// 数据获取
useEffect(() => { /* ... */ }, [url]);// 事件订阅
useEffect(() => { /* ... */ }, []);// DOM操作
useEffect(() => { /* ... */ }, [element]);
7.2 自定义 Hook 封装
function useWindowSize() {const [size, setSize] = useState({width: window.innerWidth,height: window.innerHeight});useEffect(() => {const handleResize = () => {setSize({width: window.innerWidth,height: window.innerHeight});};window.addEventListener('resize', handleResize);return () => window.removeEventListener('resize', handleResize);}, []);return size;
}
7.3 使用 useEffectEvent(未来API)
// 实验性API(React 18+)
import { useEffect, useEffectEvent } from 'react';function ChatRoom({ roomId }) {const onMessage = useEffectEvent((message) => {// 可以访问roomId但不需要声明依赖showNotification(roomId, message);});useEffect(() => {const connection = connect(roomId);connection.on('message', onMessage);return () => connection.disconnect();}, [roomId]); // 只依赖roomId
}
八、案例
8.1 数据获取与取消
function ProductDetails({ productId }) {const [product, setProduct] = useState(null);const [error, setError] = useState(null);useEffect(() => {let isCancelled = false;const fetchData = async () => {try {const data = await fetchProduct(productId);if (!isCancelled) {setProduct(data);}} catch (err) {if (!isCancelled) {setError(err);}}};fetchData();return () => {isCancelled = true;};}, [productId]);// ...
}
8.2 滚动位置恢复
function ScrollPosition({ page }) {const scrollRef = useRef(null);// 保存滚动位置useEffect(() => {const handleScroll = () => {sessionStorage.setItem(`scroll-${page}`, scrollRef.current.scrollTop);};scrollRef.current.addEventListener('scroll', handleScroll);return () => {scrollRef.current.removeEventListener('scroll', handleScroll);};}, [page]);// 恢复滚动位置useEffect(() => {const savedPosition = sessionStorage.getItem(`scroll-${page}`);if (savedPosition) {scrollRef.current.scrollTop = Number(savedPosition);}}, [page]);return <div ref={scrollRef} className="scroll-container">...</div>;
}
8.3 动画控制
function FadeInElement({ children }) {const ref = useRef(null);useEffect(() => {const element = ref.current;// 初始状态element.style.opacity = '0';element.style.transform = 'translateY(20px)';element.style.transition = 'opacity 0.5s, transform 0.5s';// 触发动画requestAnimationFrame(() => {element.style.opacity = '1';element.style.transform = 'translateY(0)';});return () => {// 清理样式element.style.opacity = '';element.style.transform = '';element.style.transition = '';};}, []);return <div ref={ref}>{children}</div>;
}
九、useEffect 与类组件生命周期对比
类组件生命周期 | useEffect 等效实现 | 说明 |
---|---|---|
componentDidMount | useEffect(fn, []) | 空依赖数组 |
componentDidUpdate | useEffect(fn) 或 useEffect(fn, [dep]) | 无依赖或有依赖数组 |
componentWillUnmount | useEffect(() => { return cleanupFn }, []) | 返回清理函数 |
shouldComponentUpdate | 无直接等效,使用 React.memo 或 useMemo | 优化渲染 |
十、总结与最佳实践
10.1 核心原则
- 副作用分离:每个 effect 只做一件事
- 依赖精确:确保依赖数组包含所有变化的值
- 清理资源:返回清理函数避免内存泄漏
- 避免阻塞:不要在 effect 中执行同步耗时操作
10.2 最佳实践清单
- ✅ 使用多个 useEffect 分离不同副作用
- ✅ 为每个 effect 添加精确的依赖数组
- ✅ 返回清理函数取消订阅/定时器
- ✅ 使用自定义 Hook 复用副作用逻辑
- ✅ 避免在 effect 中直接修改 DOM(使用 ref)
- ✅ 使用
useCallback
/useMemo
优化依赖项 - ✅ 处理异步操作的取消和清理
10.3 常见错误避免
- ❌ 遗漏依赖导致过时闭包
- ❌ 忘记清理订阅/定时器
- ❌ 在 effect 中执行阻塞渲染的操作
- ❌ 过度使用无依赖 effect 导致性能问题
- ❌ 在渲染期间设置状态导致无限循环