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 的骚操作。

相关推荐
小p1 小时前
react学习12:状态管理redux
前端·react.js
AAA阿giao1 小时前
深入理解 JavaScript 中的 Symbol:独一无二的“魔法钥匙”
前端·javascript·ecmascript 6
晴栀ay1 小时前
JS面向对象:从"猫"的视角看JavaScript的OOP进化史
前端·javascript·面试
lichong9511 小时前
Android 弹出进度条对话框 避免用户点击界面交互
java·前端·javascript
ycgg1 小时前
别再只用 --xxx!CSS @property 解锁自定义属性的「高级玩法」
前端·css
JHC0000001 小时前
47. 全排列 II
开发语言·python·面试
灵犀坠1 小时前
前端知识体系全景:从跨域到性能优化的核心要点解析
前端·javascript·vue.js·性能优化·uni-app·vue
超哥的一天1 小时前
【前端】每天一个知识点-NPM
前端·node.js
海边的云1 小时前
vue对接海康摄像头-H5player
前端