它是干嘛的

和外部系统同步:DOM、网络、订阅、定时器、第三方库。

useEffect(() => {
  // 副作用
  return () => {
    // 清理(组件卸载或依赖变前跑)
  };
}, [dep1, dep2]);

执行时机:commit 之后、浏览器绘制后

依赖数组的三种写法

useEffect(fn);          // 每次 render 都跑——基本没用
useEffect(fn, []);      // 只跑一次(挂载时)+ 清理跑一次(卸载时)
useEffect(fn, [a, b]);  // 任一变化时跑(+ 上次的清理)

ESLint 会逼你写所有依赖

useEffect(() => {
  fetch(`/api/${id}`).then(...);
}, []);   // ⚠️ eslint-plugin-react-hooks 会提示 "id 缺少"

遵守它——少写依赖几乎一定有 bug。

大部分时候你不需要 useEffect

官方文档整整一章在讲这个。

❌ 用 effect 派生 state:

useEffect(() => setFullName(`${first} ${last}`), [first, last]);

✅ render 里算:

const fullName = `${first} ${last}`;

❌ 用 effect 在事件之后做事:

function Form() {
  const [submitted, setSubmitted] = useState(false);
  useEffect(() => { if (submitted) showToast(); }, [submitted]);
  const onSubmit = () => setSubmitted(true);
}

✅ 事件回调里直接做:

const onSubmit = () => { showToast(); };

✅ 用 effect 的场合:

  • 订阅外部数据(WebSocket、EventEmitter)
  • 集成第三方库(地图、图表、富文本)
  • 浏览器 API(document.title、IntersectionObserver)
  • 数据获取(但 Server Components / TanStack Query / SWR 更好)

清理函数

useEffect(() => {
  const ws = new WebSocket(url);
  ws.onmessage = ...;
  return () => ws.close();    // 卸载前 / 依赖变前先关
}, [url]);

没写清理 = 内存泄漏 + 重复订阅。订阅类副作用必带清理。

竞态条件

useEffect(() => {
  fetch(`/api/${id}`).then(r => r.json()).then(setData);
}, [id]);

id 从 1 切到 2,两个请求都飞了;1 的响应可能比 2 晚到 → UI 显示 1 的数据但 id 是 2。

修法

useEffect(() => {
  let cancelled = false;
  fetch(`/api/${id}`).then(r => r.json()).then(data => {
    if (!cancelled) setData(data);
  });
  return () => { cancelled = true; };
}, [id]);

或用 AbortController

useEffect(() => {
  const ctrl = new AbortController();
  fetch(`/api/${id}`, { signal: ctrl.signal }).then(...).catch(...);
  return () => ctrl.abort();
}, [id]);

或——用 TanStack Query,竞态它内部处理。

StrictMode 下 effect 跑两次

开发模式 + <StrictMode> 故意把 effect 跑两遍(挂载 → 清理 → 重新挂载),就是为了暴露你没正确清理。生产环境只跑一次。

如果你的代码挂两次会出 bug——bug 不在 React,在你。

经典反模式

// 模拟 componentDidMount
useEffect(() => { ... }, []);

心态错误。"我想在挂载时跑一次"通常意味着:

  • 应该改成 Server Component 在服务端跑
  • 或者数据来源应该是父组件传 prop
  • 或者用专门的数据获取库

直接看 react.dev/learn/you-might-not-need-an-effect,把例子过一遍。

自检清单

写 useEffect 前问自己:

  1. 这件事能不能在 render 里直接算?
  2. 这件事能不能放事件回调?
  3. 我清理了吗?
  4. 我处理竞态了吗?
  5. 依赖数组完整吗?

→ 下一篇 useRef 与命令式句柄