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

193577746(kyriewen) · July 27, 2026 · 11 hits

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

你大概也是这样写的

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

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*完全多余*。

const [items, setItems] = useState([]);
const [keyword, setKeyword] = useState('');

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

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

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

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

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

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

useEffect(() => {
  setTotal(cart.reduce((sum, item) => sum + item.price * item.qty, 0));
}, [cart]);
// ✅ 直接计算
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 变化来执行"事件逻辑"。

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

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

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

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

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

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

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

这个最经典了:

// ❌ 裸写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 行代码。还没处理缓存、重试、去重、竞态条件……

// ✅ 用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 初始化只执行一次的逻辑

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

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

// ✅ 模块级初始化
let initialized = false;

function App() {
  if (!initialized) {
    initSDK({ appId: 'xxx' });
    registerGlobalHandler();
    initialized = true;
  }

  return <div>...</div>;
}

或者用更优雅的方式:

// ✅ 用useRef保证只执行一次
const didInit = useRef(false);

useEffect(() => {
  if (didInit.current) return;
  didInit.current = true;

  initSDK({ appId: 'xxx' });
  registerGlobalHandler();
}, []);

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

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

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

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

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

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

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

那什么时候该用 useEffect?

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

// ✅ 正确用法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?检查完之后能删掉几个?评论区聊聊。

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.