react 副作用探究

react 中的副作用

什么是副作用?

  • 在函数式编程里,副作用(side effect)指函数除了返回值之外,对外部世界产生的影响:修改外部变量、发请求、操作 DOM、读写存储、订阅/计时器等。

  • 在 React 中,渲染(render)必须是纯的:只根据 props/state 计算出要渲染的 UI,不做任何副作用;一切与外界交互都要放到 React 的"提交阶段(commit)"里,通过 Effect 等机制完成。

在 React 里,哪些是副作用

常见副作用清单(应该放到 effect 或事件中,而非 render):

  • 数据请求/写入:fetch、axios、IndexedDB、本地存储(localStorage/sessionStorage)。
  • 订阅/连接:WebSocket、SSE、EventSource、自定义事件、RxJS 订阅。
  • 计时器:setTimeout/setInterval、requestAnimationFrame。
  • DOM 操作:读写布局、手动添加/移除事件、第三方非 React 组件初始化/销毁。
  • 日志/埋点:console、监控上报。
  • 突变外部状态:修改全局变量、单例、window、document、全局缓存等。
  • 路由跳转:navigate(通常在事件里做,或在 effect 中根据状态变化做)。

为什么不能在渲染中做副作用

  • 渲染可能多次被中断回滚 (React 18 并发特性 + StrictMode 开发模式会故意二次调用组件的 render / mount,以帮助你发现"不纯"的副作用)。
  • 如果在 render 里发请求/改 DOM,会重复执行、产生竞态甚至内存泄漏。

该把副作用放哪

优先顺序(官方推荐):

  1. 事件处理器 (最好):

    用户触发才发生的事(点击发请求/写入存储/上报),直接放 onClick/onChange;不占用渲染生命周期,简单、可控。

  2. useEffect (渲染后、异步执行,不阻塞绘制):

    根据 state/props 的变化去"同步外部世界":加载数据、订阅、计时器、写存储、日志等。支持清理。

  3. useLayoutEffect (DOM 提交后、绘制前 、同步执行):

    需要测量布局同步修正 DOM(避免闪烁)时用,比如读取元素尺寸然后立即设置样式/滚动。

  4. useInsertionEffect (极早期,用于样式注入库):

    一般只给 CSS-in-JS 库用,业务几乎不用。

useEffect 的工作方式(要点)

  • 时机:浏览器完成绘制后再执行,不阻塞渲染。
  • 依赖数组 :决定 effect 何时运行/重跑;漏依赖会导致"陈旧闭包"
  • 清理函数:返回函数在下次重跑前或组件卸载时执行(用来取消订阅、清计时器、Abort 请求)。
  • React 18 StrictMode(仅开发):会 mount → cleanup → 再 mount,帮你检查 effect 是否能正确清理;生产环境只执行一次。

示例(请求 + 竞态处理):

tsx 复制代码
useEffect(() => {
  const ctrl = new AbortController();
  (async () => {
    try {
      const res = await fetch(`/api/posts?user=${userId}`, { signal: ctrl.signal });
      if (!res.ok) throw new Error('Network');
      const data = await res.json();
      setPosts(data);
    } catch (e) {
      if ((e as any).name !== 'AbortError') setError(e as Error);
    }
  })();
  return () => ctrl.abort();
}, [userId]); // 别漏依赖

useEffect vs useLayoutEffect

useEffect :不阻塞绘制;适合网络、订阅、日志等绝大多数副作用。
useLayoutEffect :在绘制前同步执行;适合读写布局、同步 DOM 修正,否则可能闪烁。SSR 场景会有告警(可在客户端条件使用)。

示例(测量 DOM):

tsx 复制代码
const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
  const r = ref.current!;
  const { width } = r.getBoundingClientRect();
  r.style.height = width * 0.5625 + 'px';
}, []);

常见陷阱与最佳实践

  1. 事件里能做的,就别放 effect

    例如点击"加载更多"→ 在 onClick 里请求即可,而不是 watch 某个 state 再在 effect 请求。

  2. 拆分 effect

    不相关的副作用分开写,避免交叉依赖和重复触发。

  3. 依赖数组写全

    开启 eslint-plugin-react-hooks;如果因性能原因不想重跑,考虑把不稳定的函数用 useCallback、对象用 useMemo用 ref 保存可变值

  4. 避免在 effect 里无条件 setState 造成死循环

    如果 setState 基于异步结果/条件变化,确保依赖正确且不会每次导致同样的更新。

  5. 处理异步竞态

    组件在请求未完成前卸载或依赖变化,要 Abort 或比对请求标识,避免"后到的旧数据覆盖新数据"。

  6. 第三方库初始化/销毁

    在 effect 里初始化实例,返回清理函数销毁/解绑事件。

  7. SSR
    useEffect 不在服务端执行;useLayoutEffect 在 SSR 下会告警,按需改为条件调用或仅在客户端渲染。

  8. 把"派生数据"放到 memo 而不是 effect

    纯计算(由现有 state/props 得出)用 useMemo,不要用 effect 去 setState 派生,这会多一次渲染并可能出错。

该放事件还是 effect?(速判)

用户动作直接触发 :事件(onClick/onSubmit...)。
状态/路由变化带来的外部同步 :effect。
需要读/改布局 :layout effect。
只是算个新值:memo,而不是 effect + setState。

实用片段

订阅

tsx 复制代码
useEffect(() => {
  const sub = bus.on('message', setMsg);
  return () => sub.off(); // 清理
}, []);

计时器

tsx 复制代码
useEffect(() => {
  const id = setInterval(() => setTick(t => t + 1), 1000);
  return () => clearInterval(id);
}, []);

写本地存储(响应 state 变化)

tsx 复制代码
useEffect(() => {
  localStorage.setItem('theme', theme);
}, [theme]);

避免陈旧闭包(把可变值放 ref)

tsx 复制代码
const latestFilter = useRef(filter);
useEffect(() => { latestFilter.current = filter; }, [filter]);

useEffect(() => {
  const id = setInterval(() => {
    doSearch(latestFilter.current); // 始终拿到最新 filter
  }, 5000);
  return () => clearInterval(id);
}, []);

一句话记忆

渲染要纯;副作用放事件或 effect;布局相关用 layout effect;依赖写全、及时清理、处理竞态。

相关推荐
小oo呆2 小时前
【自然语言处理与大模型】LangChainV1.0入门指南:核心组件Streaming
前端·javascript·easyui
Aotman_2 小时前
Vue.directive:自定义指令及传参
前端·javascript·vue.js·elementui·ecmascript·es6
wangchen_02 小时前
C++<fstream> 深度解析:文件 I/O 全指南
开发语言·前端·c++
程序员码歌2 小时前
短思考第266天,玩IP路上的几点感悟,这几点很重要!
前端·后端·创业
梵得儿SHI2 小时前
2025 Vue 技术实战全景:从工程化到性能优化的 8 个落地突破
前端·javascript·vue.js·pinia2.2·响应式数据分片·展望vue3.6·2025年vue技术栈
熊猫钓鱼>_>2 小时前
解决Web游戏Canvas内容在服务器部署时的显示问题
服务器·前端·游戏·canvas·cors·静态部署·资源路径
梦6502 小时前
React 封装 UEditor 富文本编辑器
前端·react.js·前端框架
Hao_Harrision2 小时前
50天50个小项目 (React19 + Tailwindcss V4) ✨ | DoubleClickHeart(双击爱心)
前端·typescript·react·tailwindcss·vite7
qq. 28040339842 小时前
react 编写规范
前端·react.js·前端框架