useEffect 戒断指南与“鬼畜”的双重请求之谜

author: 大布布将军

前言:午夜惊魂 Network 面板

假设前端同学==A

继上周改完 Context 的 Bug 后,A这周又遇到了新的灵异事件。

场景很经典:A写了一个简单的详情页,页面加载时请求一次 API 获取数据。代码看起来人畜无害,清纯可爱。

但是,当A打开 Chrome 的 Network 面板时,A揉了揉眼睛。同一个 API 接口,在几毫秒内被调用了两次。

A第一反应是鼠标坏了,双击了? 第二反应是后端 Socket 没握手好? 第三反应:React,是不是你小子又在搞我?

今天我们就来聊聊这个让无数前端在深夜破防的 useEffect,以及它是如何一步步变成代码里的"逻辑黑洞"的。


第一案:双重请求的"灵异"事件

先看代码,这应该是全天下 React 开发者写得最顺手的一段:

tsx 复制代码
useEffect(() => {
  console.log('组件挂载了,开始请求数据...');
  fetchUserProfile(id).then(data => setUser(data));
}, [id]);

现象: 控制台打印了两行"组件挂载了...",Network 里飞出去两个请求。

真相(React 18 Strict Mode): 如果你的项目是用 create-react-app 或者 vite 建的,并且套了一层 <React.StrictMode>,在 开发环境(Development) 下,React 会故意搞你心态。

它会执行:Mount(挂载) -> Unmount(卸载) -> Mount(再次挂载)

React 官方的解释是:

"我们要帮你测试,你的 Effect 里的清理函数(Cleanup Function)是不是写对了。如果你的组件卸载时不清理垃圾,我就把它重新挂载一遍,让你看看会不会出 Bug。"

怎么解决?

  1. 鸵鸟战术(不推荐) :去 index.js<StrictMode> 删了。世界清静了,但你就失去了一个帮你检查内存泄漏的保镖。
  2. AbortController(正道的光) : 在 useEffect 里写上清理逻辑,取消上一次请求。
useEffect(() 复制代码
  const controller = new AbortController();

  fetchUserProfile(id, { signal: controller.signal })
    .then(data => setUser(data))
    .catch(err => {
      if (err.name !== 'AbortError') {
         // 处理真正的错误
      }
    });

  return () => {
    // 组件卸载(或者由于 Strict Mode 导致的假卸载)时,取消请求
    controller.abort();
  };
}, [id]);

老司机心里话: 其实绝大多数时候,在开发环境请求两次根本无所谓。只要你的后端接口是幂等的(查询通常都是),除了看着心烦,上线后(生产环境)它是不会跑两次的。

第二案:useEffect 是怎么变成"逻辑黑洞"的?

比双重请求更可怕的,是把 useEffect 当作逻辑胶水来用。

也就是传说中的"连锁反应(Chain Reaction)"

假设有一个表单:用户选了"国家",自动重置"省份";选了"省份",自动重置"城市"。

❌ 菜鸟写法(Effect 连锁):

// 复制代码
useEffect(() => {
  setProvince('');
}, [country]);

// 监听 province,变了就改 city
useEffect(() => {
  setCity('');
}, [province]);

// 监听 city,变了就去请求区号...
useEffect(() => {
  if(city) fetchAreaCode(city);
}, [city]);

这有什么问题? 这简直就是玩弹珠台!

  1. 用户改了 country -> Render -> 触发 Effect 1。
  2. Effect 1 修改 province -> Render -> 触发 Effect 2。
  3. Effect 2 修改 city -> Render -> ...

React 必须跑好几轮渲染才能把状态稳住。代码一旦复杂起来,这种依赖链条就像纠缠在一起的耳机线,根本理不清数据流向。

✅ 高手写法(在事件中处理):

永远记住一句话:如果是用户操作引起的改变,就在事件处理函数里写,别在 Effect 里写。

const 复制代码
  setCountry(newCountry);
  // 直接在这里重置,不要等 Effect
  setProvince('');
  setCity(''); 
};

这就叫Batch Update(批处理) 。一次点击,改好所有状态,React 只需一次 Render 就能把正确的 UI 画出来。清晰、高效、无副作用。

第三案:由数据推导数据的"脱裤子放屁"

还有一个重灾区:冗余状态

❌ 错误示范:

const 复制代码
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('');

// 兄弟,你这是在用 React 算 1+1=2 吗?
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

这里 fullName 根本不需要是 State!因为它可以直接由 firstNamelastName 计算出来。 你这么写,又是多一次 Render。

✅ 正确示范:

const 复制代码
const [lastName, setLastName] = useState('Doe');

// 在渲染过程中直接计算
// 只要组件重渲染,fullName 自动就是最新的
const fullName = `${firstName} ${lastName}`;

如果计算很昂贵(比如要遍历几千个数组),那就用 useMemo 包一下,但千万别用 useEffectsetState

总结:该把 useEffect 关进笼子了

React 官方文档现在都在极力劝退 useEffect

为什么? 因为 useEffect 的本意是: "将组件与外部系统(Network, DOM, Subscription)同步" 。 它不是 用来监听数据变化的,也不是用来做逻辑流转的。

当你准备写下 useEffect 之前,请先问自己三个问题:

  1. 这是为了和后端/外部同步吗? 是 -> 写吧。
  2. 这是用户交互(点击/输入)导致的吗? 是 -> 写在 Event Handler 里。
  3. 这是可以用现有的 Props/State 算出来的吗? 是 -> 直接算,或者用 useMemo

useEffect 用在刀刃上,你的代码会少一半 Bug,你的发际线会后退得慢一点。

好了,A要去把那个在 useEffect 里写 setInterval 还没有清除定时器的实习生代码改了,不然浏览器内存要炸了。


下期预告 :你以为 useRef 只是用来获取 DOM 节点的吗?错了,它是 React 函数式组件里唯一的"逃生舱"和"时光胶囊"。下一篇,带你解锁 useRef 的骚操作。

相关推荐
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
晨米酱1 天前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
卡布鲁1 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
彧azz1 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
汉堡大王95271 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大1 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端