1. 首页
  2. React

useEffect和useCallback再乱写,你的代码就要炸了!

我们经常会看到这样的场景:一个组件改了 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让陈年老请求自闭!

seEffect(() => {
  let ignore = false;
  fetchResults(query).then(json => {
    if (!ignore) setResults(json); // 只认最后一个请求!
  });
  return () => { ignore = true; }; // 清理函数一键截胡!
}, [query]);

原理:Effect卸载时标记ignore=true,过时响应直接原地丢弃!

代码逐行解释

useEffect(() => { … }, [query]);

这是一个副作用 Hook。它告诉 React:在组件挂载后,或者依赖项数组 [query]中的 query值发生变化后,执行我传入的函数(即副作用函数)。

let ignore = false;

在副作用函数的开头,声明一个局部变量 ignore,并初始化为 false。这个变量是本次特定渲染周期(即本次 query值所对应的渲染)的“标志位”。它用于标记当前发起的这个请求是否已经被“废弃”。

fetchResults(query).then(json => { … });

根据当前的 query发起一个异步网络请求。

当请求成功返回数据(json)时,执行 .then中的回调函数。

回调函数:if (!ignore) setResults(json);

在回调函数中,首先检查 ignore标志位。

如果 ignore为 false:表示这个请求仍然是“有效的”(即它是最后一次发起的请求),那么就调用 setResults(json)来更新组件的状态,从而触发重新渲染并显示新数据。

如果 ignore为 true:表示这个请求已经被“废弃”(即在它返回之前,已经有新的请求因 query变化而发起),那么就什么都不做,直接忽略这个响应。

清理函数:return () => { ignore = true; };

这是 useEffect的关键特性。副作用函数可以返回一个清理函数。

执行时机: 在组件卸载时

在下一次执行本副作用函数之前(即依赖项 query再次改变,触发新一轮渲染和新的副作用时)。

作用:当 query变化,导致下一次副作用要执行时,React 会先执行上一次副作用的清理函数。在这个清理函数中,它将上一次渲染周期中定义的 ignore变量设置为 true。

核心流程与“竞态条件”解决

让我们通过一个用户快速输入“apple”,然后改为“banana”的例子来模拟整个过程:

用户输入 a,query变为 “a”

组件渲染,副作用执行。

ignore = false(针对 "a"请求)。

发起请求 fetchResults(“a”)。

React 记住了清理函数 () => { ignore = true; }(这个 ignore指向 "a"请求的标志位)。

用户快速输入 banana,query变为 “banana”

query变化,触发重新渲染。

在执行新的副作用(用于 “banana”)之前,React 先执行上一次的清理函数。

清理函数将 "a"请求的 ignore标志位设置为 true。这意味着 "a"请求已经被“废弃”。

新的副作用执行(针对 “banana”)

ignore = false(这是一个新的变量,针对 "banana"请求)。

发起新的请求 fetchResults(“banana”)。

注册新的清理函数。

两个请求陆续返回

"a"的请求返回:它的回调函数检查 ignore,发现已经是 true(因为在第2步被设置了),于是 if (!ignore)条件不成立,setResults不会被调用。过时的数据被成功丢弃!

"banana"的请求返回:它的回调函数检查 ignore,发现是 false,于是正常调用 setResults(json),更新界面显示正确的结果。

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子组件直接跳过渲染!

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


TOP