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。


  1. 交互时直接调(推荐)
const handleClick = async () => {
  setLoading(true);
  const data = await fetchData(params);
  setData(data);
  setLoading(false);
};

优点

  • 时序直观:点击 → 立刻发请求,不用等渲染+effect 再调一次
  • 避免无限循环 / 条件写错
  • 更容易取消 / 防抖 / 重试
  • SSR/测试里不会意外调接口

  1. useEffect 监听变化(仅限“外部驱动”)
useEffect(() => {
  if (!id) return;
  const ctrl = new AbortController();
  fetchData(id, ctrl.signal).then(setData);
  return () => ctrl.abort();
}, [id]); // 来自 url 或 props 的派生值

适用场景

  • 路由参数、查询字符串、父组件 props 等非用户事件直接产生的变化
  • 需要自动补数据(进入页面即根据初始 id 拉一次)

  1. 混合模式(最佳实践缩影)
// 初始/外部变化用 effect
useEffect(() => { fetchPageData(page); }, [page]);

// 用户主动换页直接调
const goPage = (p) => {
  setPage(p);        // 同步更新地址栏、按钮高亮
  fetchPageData(p);  // 立即发请求,省一次渲染
};

决策表

触发源 推荐方式
点击、输入、提交等用户行为 事件处理器里直接调
路由参数、props、url 变化 useEffect 监听
两者都有 事件处理器里直接调 + effect 做兜底/初始化

总结
“能顺手发就别监听”——effect 只负责“没人点它也要跑”的场景;
用户主动操作就在事件回调里一把梭,代码更少、行为更明确、不会踩坑。

React 团队不是让你少用 useEffect,是希望你更聪明地用 React 思维去解决问题。我们应该把副作用留给真正副作用的场景(如订阅、事件、请求) ,而不是用它来弥补对组件模型的理解盲区。

posted @ 2025-09-08 11:21  丁少华  阅读(11)  评论(0)    收藏  举报