读完 Dan 的 useEffect 指南,我总结了这些

0 阅读3分钟

Dan Abramov 那篇《A Complete Guide to useEffect》很长,但核心就一句话:别再用生命周期的思维想 useEffect,它的本质是"同步"。

下面是我提炼的几个关键认知,搞懂这些,useEffect 的坑基本踩不到。

一、每次渲染都是一张"快照"

函数组件每次渲染,props、state、事件处理函数、effect,全都是那一次渲染的快照。count 不是一个随时间变化的变量,它就是一个常量。

function Counter() {
  const [count, setCount] = useState(0);
  
  function handleClick() {
    setTimeout(() => {
      alert(count); // 永远是点击时的值,不是最新值
    }, 3000);
  }
}

点击时 count 是 3,哪怕 3 秒内变成了 5,弹出来的还是 3。因为这个函数"拍"下来的就是那一刻的 count。

useEffect 也一样。每次渲染的 effect 函数,看到的都是当次渲染的 props 和 state。

二、依赖数组是"承诺",不是"优化"

很多人把 [] 当成"只运行一次"的开关。错了。  依赖数组是你对 React 的承诺:"我这个 effect 只用到了这些变量。"

如果 effect 里用了 count,但依赖写了 [],你就是在对 React 撒谎。后果是 effect 永远看到初始值,产生闭包陷阱。

规则很简单:effect 里用到什么,依赖就写什么。不要手动"优化"依赖。

三、依赖太多怎么办?减少依赖,而不是撒谎

当你发现依赖数组越写越长,或者 effect 频繁触发时,正确做法不是删依赖,而是重构代码减少依赖。

方法一:函数式更新

// ❌ 依赖 count,定时器每秒重建
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

// ✅ 不依赖 count,告诉 React "怎么更新"
useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);

方法二:useReducer

当更新逻辑依赖多个状态时,用 useReducer 把逻辑收拢。dispatch 的引用永远稳定,天然不需要作为依赖。

const [state, dispatch] = useReducer(reducer, initialState);

useEffect(() => {
  const id = setInterval(() => {
    dispatch({ type: 'tick' }); // 不依赖任何 state
  }, 1000);
  return () => clearInterval(id);
}, []);

方法三:处理函数依赖

函数每次渲染都重新创建,放进依赖会导致 effect 每次都跑。三种解法:

场景做法
函数不用 props/state移到组件外面
函数只在 effect 里用移到 effect 里面
函数在多处使用用 useCallback 包一层

四、清理函数看到的是"旧值"

清理函数不等于 componentWillUnmount。它的执行时机是:

React 渲染新 UI

浏览器绘制

运行上一次的清理函数(看到旧 props/state)

运行这一次的 effect(看到新 props/state)

理解这个顺序,就知道为什么清理函数能正确"收拾"上一次的副作用。

五、异步请求防竞态

如果 effect 里有异步请求,ID 变了但旧请求先返回,就会数据错乱。用一个布尔标志位解决:

useEffect(() => {
  // 1. 定义一个“取消标志”,只在当前 effect 内部有效
  let cancelled = false;

  async function fetchData() {
    // 2. 发起异步请求
    const result = await API.fetch(id);

    // 3. 如果没有被取消,才更新数据
    if (!cancelled) {
      setData(result);
    }
  }

  fetchData();

  // 4. effect 清理函数:
  //    组件卸载 / id 变化 / effect 重新执行时,会先跑这里
  return () => {
    cancelled = true;
  };
}, [id]); // 依赖 id:id 一变,就重新执行

核心逻辑(超级关键)

  1. 每次 id 变化,都会生成一个新的 effect

  2. 旧的 effect 会先执行清理函数:
    cancelled = true

  3. 旧请求即使之后返回,看到 cancelled === true,就不会执行 setData

  4. 只有最新一次的 effect 里的 cancelled 是 false,才会真正更新数据

一句话总结:
只让最后一次请求生效,前面的请求就算回来也扔掉。

**
**

不要想"什么时候该运行 effect",要想"这个 effect 要和哪些状态保持同步"。

旧思维关注过程(挂载了、更新了),新思维关注结果(不管怎么变,UI 和副作用要和当前状态一致)。当你这么想的时候,useEffect 就不再是玄学了。

原文链接:A Complete Guide to useEffect — Dan Abramov

qrcode_for_gh_6a9e7f3719d6_344.jpg