在 React 开发中,useEffect 是我们最常用却最容易“似懂非懂”的 Hook。很多文章告诉你它的定义:

useEffect 用来处理副作用,比如数据请求、事件绑定、DOM 操作…”

但是,当你真正开发一个复杂应用时,往往会遇到这些问题:

  • 为什么有时候它会 执行两次
  • 为什么我把它的依赖数组写错,就会触发 无限循环渲染
  • 它和 useLayoutEffect、生命周期函数到底有什么区别?
  • 有没有什么 创新的用法,不仅仅是“请求数据”那么简单?

今天,我们不走寻常路,带你从“时间”视角理解 useEffect,并探索一些创新用法。


🌱 一、为什么叫 useEffect —— 副作用的本质

在数学中,函数的纯粹性意味着:

输入一样,输出永远一样,没有额外影响。

React 组件本质上是一个“纯函数”,接收 propsstate → 返回 UI

但现实应用中,我们要 请求数据、操作 DOM、绑定事件 —— 这些都不是“纯的”,它们会产生“副作用(Side Effects)”。

所以,React 把这些“不纯”的逻辑放在 useEffect 里,让 渲染副作用解耦。

你可以理解为:
👉 render 是菜谱(描述 UI),
👉 useEffect 是炒菜(真正执行操作)。


🔄 二、useEffect 与类组件生命周期对照

React Hooks 出现之前,我们常常用 类组件生命周期函数 来处理副作用:

类组件生命周期函数 函数组件中等效写法
componentDidMount(挂载完成) useEffect(() => { ... }, [])
componentDidUpdate(更新完成) useEffect(() => { ... }, [deps])
componentWillUnmount(卸载前清理) useEffect(() => { return () => {...} }, [])
  • 首次挂载 → 执行一次 effect。
  • 更新时 → 对应依赖变化再执行。
  • 卸载时 → 自动执行清理函数。

👉 总结一句:

  • useEffect = componentDidMount + componentDidUpdate + componentWillUnmount 的组合。

⏱️ 三、useEffect 的“时间线”思维

想真正理解 useEffect,一定要抓住 它的执行时机

一个 React 组件的生命周期(以函数组件为例)可以抽象成一条时间线:

依赖变化
函数执行 -> render
DOM 更新完成
useEffect 执行
等待依赖变化
useEffect cleanup -> 再执行 Effect

👉 关键点

  • 执行时机:在 DOM 更新之后才执行。
  • 清理逻辑:在下一次依赖变化或组件卸载前,执行 cleanup

举个例子:

useEffect(() => {
  console.log("订阅消息");
  return () => {
    console.log("取消订阅");
  };
}, [userId]);

时间线表现为:

  1. 首次渲染 → 执行订阅。
  2. userId 改变 → 先取消旧订阅,再订阅新用户。
  3. 卸载组件 → 执行取消订阅。

✅ 这就是 React 保证“不会内存泄漏”的秘诀。


🔄 四、依赖数组的“误区”与“创新解法”

❌ 常见误区

  1. 忘写依赖 → 状态不同步。
  2. 乱写依赖 → 无限循环。

✅ 创新解法:用 ESLint + 自定义 Hook 管理依赖

比如一个搜索场景:

useEffect(() => {
  fetch(`/api/search?q=${query}`).then(...)
}, [query])

如果我不想每次 query 改变都触发,而是“防抖”执行?

👉 我们可以写一个 带防抖的 useEffect

function useDebounceEffect(effect, deps, delay) {
  useEffect(() => {
    const handler = setTimeout(() => effect(), delay)
    return () => clearTimeout(handler)
  }, [...deps, delay])
}

// 使用
useDebounceEffect(() => {
  fetch(`/api/search?q=${query}`).then(...)
}, [query], 500)

这样就把“副作用”与“时间控制”优雅结合起来了。


⚡ 五、useEffect vs useLayoutEffect

很多人分不清:

  • useEffect:异步执行,不会阻塞浏览器绘制。
  • useLayoutEffect:同步执行,阻塞绘制。

👉 打个比方:

  • useEffect = “装修完了再擦灰尘”。
  • useLayoutEffect = “装修工人刚放下锤子,就赶紧擦灰尘”。

如果你需要 测量 DOM 的尺寸/位置,一定要用 useLayoutEffect,否则会拿到错误值。


🧩 六、useEffect 的 3 个创新应用

1. 作为“事件总线”

function useEventBus(event, handler) {
  useEffect(() => {
    window.addEventListener(event, handler)
    return () => window.removeEventListener(event, handler)
  }, [event, handler])
}

👉 实现全局事件通信,不需要 Redux。


2. 作为“资源管理器”

useEffect(() => {
  const conn = createWebSocket("wss://server")
  return () => conn.close()
}, [])

👉 自动清理,防止内存泄漏。


3. 与 IntersectionObserver 结合,做懒加载

function useOnScreen(ref, callback) {
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) callback()
    })
    observer.observe(ref.current)
    return () => observer.disconnect()
  }, [ref, callback])
}

👉 实现图片懒加载、无限滚动。


🔮 七、思维升级:useEffect = 时间的 orchestrator(调度者)

如果说 React 是 UI 的 orchestrator,那么 useEffect 就是 副作用的 orchestrator

它的使命不是“写副作用”,而是 调度副作用的执行时机和生命周期

当你这样去理解时,useEffect 不再是“杂物箱”,而是一个 副作用的时间管理器


✨ 总结

  • useEffect 解决了 纯渲染副作用 的矛盾。
  • 它的本质是 时间的管理 —— 什么时候执行、什么时候清理。
  • 创新用法:
    • 防抖副作用 (useDebounceEffect)
    • 事件总线管理
    • 资源自动清理
    • 懒加载与可视化检测

如果你把 useEffect 当作“副作用的垃圾桶”,它会让你写出一堆混乱代码;
但如果你把它当作“副作用的调度者”,它会让你的代码优雅到飞起。 🚀


更多推荐