别再乱用useEffect了——你写的10个里有8个不该存在

我最近检查了一下项目里的useEffect,数了一下:37个。然后我逐个看了一遍,发现其中29个根本不需要useEffect。删掉之后,组件渲染快了,代码短了,bug也少了。

你大概也是这样写的

先看一段"标准"的React代码------你可能每天都在写类似的东西:

tsx 复制代码
const [items, setItems] = useState([]);
const [filtered, setFiltered] = useState([]);
const [keyword, setKeyword] = useState('');

useEffect(() => {
  setFiltered(items.filter(item => item.name.includes(keyword)));
}, [items, keyword]);

看起来很合理对吧?keyword变了,重新过滤一遍。

但这个useEffect完全多余

tsx 复制代码
const [items, setItems] = useState([]);
const [keyword, setKeyword] = useState('');

const filtered = items.filter(item => item.name.includes(keyword));

三行变一行。没有useEffect,没有额外的state,没有依赖数组,没有多余的渲染。

这不是个例。大多数useEffect都是在用副作用解决本该在渲染时直接算出来的问题。

反模式一:用useEffect同步派生状态

这是最常见的滥用------把一个可以直接算出来的值,硬拆成state + useEffect。

tsx 复制代码
// ❌ 用useEffect同步
const [cart, setCart] = useState([]);
const [total, setTotal] = useState(0);

useEffect(() => {
  setTotal(cart.reduce((sum, item) => sum + item.price * item.qty, 0));
}, [cart]);
tsx 复制代码
// ✅ 直接计算
const [cart, setCart] = useState([]);

const total = cart.reduce((sum, item) => sum + item.price * item.qty, 0);

为什么第一种写法有害?

  1. 多一次渲染:cart变了 → 渲染一次 → useEffect触发setTotal → 再渲染一次。两次渲染干了一次的活
  2. 多一个state:total是cart的派生值,但你给了它独立的生命周期,就有可能出现cart和total不同步的瞬间
  3. 多一个依赖数组:多一个需要维护的东西,多一个可能写错的地方

判断标准:这个值能不能从现有的state/props直接算出来? 能算就别存。

反模式二:用useEffect响应事件

另一个高频误区------用useEffect监听state变化来执行"事件逻辑"。

tsx 复制代码
// ❌ useEffect假装事件处理
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
  if (submitted) {
    sendAnalytics('form_submit');
    showToast('提交成功');
    setSubmitted(false);
  }
}, [submitted]);

const handleSubmit = () => {
  saveForm(data);
  setSubmitted(true);
};
tsx 复制代码
// ✅ 直接在事件处理函数里做
const handleSubmit = () => {
  saveForm(data);
  sendAnalytics('form_submit');
  showToast('提交成功');
};

用useEffect处理事件有什么问题?

  1. 因果关系断裂:读代码时,你看到setSubmitted(true),根本不知道它会触发什么。逻辑分散在两个地方
  2. 执行时机不可控:useEffect在渲染后执行,不是在事件发生时执行。中间隔了一次渲染
  3. 临时state泄露:submitted这个state只是为了"传信号",没有UI含义

判断标准:这段逻辑是因为"某个事件发生了"才执行的吗? 是的话放事件处理函数里,不要绕道useEffect。

反模式三:用useEffect获取数据(没有用对工具)

这个最经典了:

tsx 复制代码
// ❌ 裸写useEffect获取数据
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  let cancelled = false;
  setLoading(true);
  
  fetchUser(userId)
    .then(data => {
      if (!cancelled) {
        setUser(data);
        setLoading(false);
      }
    })
    .catch(err => {
      if (!cancelled) {
        setError(err);
        setLoading(false);
      }
    });
    
  return () => { cancelled = true; };
}, [userId]);

一个简单的数据获取,写了20行代码。还没处理缓存、重试、去重、竞态条件......

tsx 复制代码
// ✅ 用React Query
const { data: user, isLoading, error } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

三行搞定,还自带缓存、重试、去重、窗口聚焦刷新、竞态处理。

不是说"永远不要在useEffect里fetch"。 是说如果你的项目有超过3个数据获取的场景,裸写useEffect就是在重复造一个质量更差的React Query。

特性 裸写useEffect React Query / SWR
缓存 自己写 内置
去重 自己写 内置
竞态处理 容易忘 内置
重试 自己写 内置
loading/error 3个useState 返回值直接拿
代码量 20+行 3-5行

反模式四:用useEffect初始化只执行一次的逻辑

tsx 复制代码
// ❌ useEffect空依赖"只执行一次"
useEffect(() => {
  initSDK({ appId: 'xxx' });
  registerGlobalHandler();
}, []);

看起来人畜无害,但React严格模式下会执行两次------SDK初始化两次,全局handler注册两次。

tsx 复制代码
// ✅ 模块级初始化
let initialized = false;

function App() {
  if (!initialized) {
    initSDK({ appId: 'xxx' });
    registerGlobalHandler();
    initialized = true;
  }
  
  return <div>...</div>;
}

或者用更优雅的方式:

tsx 复制代码
// ✅ 用useRef保证只执行一次
const didInit = useRef(false);

useEffect(() => {
  if (didInit.current) return;
  didInit.current = true;
  
  initSDK({ appId: 'xxx' });
  registerGlobalHandler();
}, []);

判断标准:这段逻辑跟组件的生命周期有关吗? 如果只是"应用启动时执行一次",它不属于任何组件,别放在组件里。

反模式五:用useEffect"同步"两个state

tsx 复制代码
// ❌ 用useEffect让两个state保持同步
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
tsx 复制代码
// ✅ 派生值直接算
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

const fullName = `${firstName} ${lastName}`;

这跟反模式一本质一样,但出现频率太高了,单独说一遍。

如果你觉得"A变了B也要跟着变",那B大概率不是state,是A的计算结果。

那什么时候该用useEffect?

说了5个"不该用"的,再说说真正该用的场景------和外部系统同步。

tsx 复制代码
// ✅ 正确用法1:操作DOM
useEffect(() => {
  const el = ref.current;
  const observer = new ResizeObserver(entries => {
    setWidth(entries[0].contentRect.width);
  });
  observer.observe(el);
  return () => observer.disconnect();
}, []);

// ✅ 正确用法2:WebSocket连接
useEffect(() => {
  const ws = new WebSocket(url);
  ws.onmessage = (e) => setMessages(prev => [...prev, JSON.parse(e.data)]);
  return () => ws.close();
}, [url]);

// ✅ 正确用法3:第三方库集成
useEffect(() => {
  const chart = new Chart(canvasRef.current, config);
  return () => chart.destroy();
}, [config]);

useEffect的正确用途只有一个:让React组件和React之外的东西保持同步。 DOM API、WebSocket、第三方库、浏览器API------这些才需要useEffect。

如果你的useEffect里调用的全是React自己的API(setState、其他hooks),那大概率用错了。

终极速查表

你想做什么 用useEffect? 应该怎么做
从state/props计算一个值 渲染时直接计算,或useMemo
用户点了按钮要执行逻辑 写在事件处理函数里
请求后端API React Query / SWR / 框架的loader
表单提交后发埋点 写在onSubmit里
props变了要重置组件state 给组件加key
两个state要保持同步 合并成一个state,或算派生值
应用启动时初始化SDK ⚠️ 模块级代码或useRef守卫
监听window resize useEffect + cleanup
连接WebSocket useEffect + cleanup
集成D3/Chart等第三方库 useEffect + cleanup
同步document.title useEffect

一句话总结:useEffect不是"state变了执行代码"的工具,是"和外部世界同步"的工具。

自查清单

回去看看你的项目,把每个useEffect标记一下:

  • 里面有没有setState?→ 大概率可以删
  • 是不是在响应某个事件?→ 移到事件处理函数
  • 是不是在fetch数据?→ 换React Query
  • 依赖数组是不是总出bug?→ 可能根本不该用useEffect
  • 删掉这个useEffect,组件还能正常工作吗?→ 能的话就别犹豫

删掉那些不该存在的useEffect,你的组件会变短、变快、bug更少。这不是优化------是纠正。

你的项目里有多少个useEffect?检查完之后能删掉几个?评论区聊聊。

相关推荐
Ivanqhz1 小时前
Rust &‘static str浅析
java·前端·javascript·rust
IT_陈寒2 小时前
SpringBoot这个分页坑,我踩了三天才爬出来
前端·人工智能·后端
颜酱2 小时前
05 | 召回前置准备:根据业务数据库生成各数据库(读取配置阶段)
前端·人工智能·后端
zandy10112 小时前
衡石 Agentic BI的ReAct 推理框架在 Agentic BI 中的工程化实践
前端·javascript·react.js
罗超驿2 小时前
JavaEE进阶之路:从Web架构原理到HTML标签全解析
前端·html·web·javaee
西安小哥3 小时前
从前端到AI工程师:一场跨越鸿沟的真实蜕变之旅
前端
Prince4183 小时前
侧边栏收起缩放适配方案
前端
用户7783366132113 小时前
serpbase + Cloudflare R2 边缘持久化实战
前端·人工智能
郝亚军3 小时前
webstorm如何创建vue 3.js
javascript·vue.js·webstorm