react effect使用时机
我们经常会看到这样的场景:一个组件改了 props,紧接着用 useEffect 重置 useState;或者每次函数都用 useCallback 包起来,生怕性能不够“丝滑”。其实,这些“反射式优化”不仅没有带来好处,反而拖慢了你的 UI,制造了更多不可预测的问题。
React 官方文档已经为这些常见问题给出了非常清晰的替代方案——你可能不需要 useEffect!
1. Effect重置状态
用Effect重置状态?你是在玩俄罗斯套娃渲染!
1.1 开局对比 state
❌错误示范:
function List({ items }) {
const [selection, setSelection] = useState(null);
useEffect(() => {
setSelection(null); // 每次items变就重置?渲染两次!
}, [items]);
}
后果:组件先用旧数据渲染 → 跑Effect → 再重新渲染 → 子组件连环爆炸!
✅正确姿势:直接在渲染时暴力重置!
function List({ items }) {
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null); // 一步到位,拒绝套娃!
}
}
原理:React渲染阶段直接对比props,不!要!拖!到!Effect!
1.2 用key属性竟能秒杀嵌套状态
用key属性竟能秒杀嵌套状态?90%的人不知道!
❌错误示范:
// 用户ID变了?用Effect清空评论?太Low!
useEffect(() => setComment(""), [userId]);
后果:先渲染旧数据 → 再跑Effect → 再渲染新数据 → 性能直接扑街!
✅正确姿势:给组件一个key,让它原地去世重生!
export default function Profile({ userId }) {
return <ProfilePage key={userId} userId={userId} />; // key一变,组件直接重置!
function ProfilePage({ userId }) {
const [comment, setComment] = useState(""); // 自动清空,爽!
}
}
原理:key是组件的身份证号,一变就销毁旧组件,创建新实例,状态自动归零!
2. useEffect清理函数
不写清理函数?无脑 useEffect 拉取数据也可能出锅!
❌错误示范:
useEffect(() => {
fetchResults(query).then(setResults); // 连续请求?后发的可能先到!
}, [query]);
后果:用户疯狂输入 → 请求乱序返回 → 页面显示错乱数据!
✅正确姿势:用ignore让陈年老请求自闭!
useEffect(() => {
let ignore = false;
fetchResults(query).then(json => {
if (!ignore) setResults(json); // 只认最后一个请求!
});
return () => { ignore = true; }; // 清理函数一键截胡!
}, [query]);
原理:Effect卸载时标记ignore=true,过时响应直接原地丢弃!
3. useCallback 并非万金油
很多同学以为“函数传 props 会导致子组件重新渲染”,于是每个函数都包 useCallback,但其实:
- 如果组件没有对该函数依赖进行 memo 或 React.memo 优化,useCallback 根本不会带来任何性能提升;
- 而多余的 useCallback 会增加思维负担,甚至带来 bugs(依赖数组写错了都不知道)。
❌错误示范:
const handleSubmit = (orderDetails) => { /* ... */ };
// 每次渲染都创建新函数,memo子组件白给了!
<ShippingForm onSubmit={handleSubmit} />
后果:子组件疯狂重渲染 → 性能优化了个寂寞!
✅正确姿势:useCallback锁死函数,依赖不变就复用!
const handleSubmit = useCallback((orderDetails) => {
post(`/products/${productId}/buy`, [referrer, orderDetails]);
}, [productId, referrer]); // 依赖不变,函数永远缓存!
原理:useCallback让函数身份不变,memo子组件直接跳过渲染!
- 组件内部自用的函数(没往下传)——没必要包
useCallback; - 只有当把函数作为 prop 传给子组件,且子组件用 React.memo 包裹时,才值得用
useCallback避免子组件因“新函数引用”而无谓重渲染。
一句话记忆:
“不传不包,传了再包;没 memo,包了白包。”
4. 接口请求
react中页面交互会导致某些状态变化,然后状态变化会有对应的接口请求,请问这个接口请求是通过effect监听变化触发的好,还是交互的时候一边修改状态,一边手动调用接口请求好呢?
“交互时直接调用” > “用 useEffect 监听变化再调”
——除非你是纯消费派生状态(比如 url query 变化、上游 props 变化)才考虑 effect。
- 交互时直接调(推荐)
const handleClick = async () => {
setLoading(true);
const data = await fetchData(params);
setData(data);
setLoading(false);
};
优点
- 时序直观:点击 → 立刻发请求,不用等渲染+effect 再调一次
- 避免无限循环 / 条件写错
- 更容易取消 / 防抖 / 重试
- SSR/测试里不会意外调接口
- useEffect 监听变化(仅限“外部驱动”)
useEffect(() => {
if (!id) return;
const ctrl = new AbortController();
fetchData(id, ctrl.signal).then(setData);
return () => ctrl.abort();
}, [id]); // 来自 url 或 props 的派生值
适用场景
- 路由参数、查询字符串、父组件 props 等非用户事件直接产生的变化
- 需要自动补数据(进入页面即根据初始 id 拉一次)
- 混合模式(最佳实践缩影)
// 初始/外部变化用 effect
useEffect(() => { fetchPageData(page); }, [page]);
// 用户主动换页直接调
const goPage = (p) => {
setPage(p); // 同步更新地址栏、按钮高亮
fetchPageData(p); // 立即发请求,省一次渲染
};
决策表
| 触发源 | 推荐方式 |
|---|---|
| 点击、输入、提交等用户行为 | 事件处理器里直接调 |
| 路由参数、props、url 变化 | useEffect 监听 |
| 两者都有 | 事件处理器里直接调 + effect 做兜底/初始化 |
总结
“能顺手发就别监听”——effect 只负责“没人点它也要跑”的场景;
用户主动操作就在事件回调里一把梭,代码更少、行为更明确、不会踩坑。
React 团队不是让你少用 useEffect,是希望你更聪明地用 React 思维去解决问题。我们应该把副作用留给真正副作用的场景(如订阅、事件、请求) ,而不是用它来弥补对组件模型的理解盲区。

浙公网安备 33010602011771号