useEffect 依赖数组到底怎么用?

这一块儿面试问得很多,今天彻底搞明白

先说结论:useEffect(fn, deps) 什么时候执行,全看 deps 数组怎么写。

三种写法,三种效果

1.useEffect(fn, []) — 空数组

只在两个时候执行:

  • 组件挂载后执行一次
  • 组件卸载前执行清理函数
useEffect(() => {
  console.log("我只会执行一次!");
  return () => console.log("组件要卸载了");
}, []);

典型场景:接口请求、绑定事件、初始化第三方库。

相当于类组件的 componentDidMount + componentWillUnmount

2.useEffect(fn) — 不写第二个参数

每次渲染都执行,不管有没有变化。

useEffect(() => {
  console.log("每次渲染我都跑");
});

这货基本不用,除非你在调试或者真的需要每次渲染都同步点什么。

3.useEffect(fn, [count, userId]) — 写依赖数组

执行时机:

  • 组件挂载后执行一次
  • 依赖项变化时执行
useEffect(() => {
  console.log("count 变了");
}, [count]); // 只有 count 变时才执行

铁律:必须诚实填写依赖

在 effect 里用到的所有响应式数据(state、props、context),都必须填进依赖数组。

忘了填就会出 bug——这就是著名的闭包陷阱

闭包陷阱是咋回事?

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count); // 永远打印 0!
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 空依赖!

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

因为 [] 空依赖,effect 只在第一次渲染时执行。那时候 count 是 0,定时器闭包捕获的就是这个 0。后面 count 变成 1、2、3... 定时器里还是 0。

怎么解?

写法1:把依赖加上

useEffect(() => {
  const timer = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(timer);
}, [count]); // count 变了就重建定时器

缺点:每次 count 变,定时器都要销毁重建,浪费。

写法2:函数式更新(推荐)

useEffect(() => {
  const timer = setInterval(() => {
    setCount((c) => c + 1); // 不依赖外部变量!
  }, 1000);
  return () => clearInterval(timer);
}, []); // 稳定了

setCount(c => c + 1) 而不是 setCount(count + 1),就不需要依赖 count 了。

写法3:用 ref

function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(countRef.current); // 永远是最新的
    }, 1000);
    return () => clearInterval(timer);
  }, []);

  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

进阶:依赖是个对象怎么办?

如果你依赖一个对象:

function Child({ config }) {
  useEffect(() => {
    console.log("config 变了");
  }, [config]); // 问题来了!
}

父组件每次渲染,如果不加 useMemoconfig 都是新对象:

function Parent() {
  const config = { color: "red" }; // 每次渲染都是新对象
  return <Child config={config} />;
}

这会导致 useEffect 每次都执行,即使 config 内容没变。

解法

1. 用 useMemo 缓存

function Parent() {
  const config = useMemo(() => ({ color: "red" }), []);
  return <Child config={config} />;
}

2. 只依赖具体值

useEffect(() => {
  console.log("config 变了");
}, [config.color]); // 只依赖具体属性

总结

写法什么时候执行
useEffect(fn, [])只在挂载时执行一次
useEffect(fn, [dep])挂载 + dep 变化时
useEffect(fn)每次渲染都执行

核心原则:在 effect 里用了啥,就诚实地填啥。别想着偷懒用 eslint-disable,不然 bug 找上门。记住,依赖数组就是你和 React 之间的信任。