🎯 React Hooks 从入门到精通:掌握现代 React 开发的核心利器(含 useEffect 依赖项深度解析)

56 阅读5分钟

🎯 React Hooks 从入门到精通:掌握现代 React 开发的核心利器(含 useEffect 依赖项深度解析)

在现代前端开发中,React Hooks 已经成为构建高效、可维护组件的标配。自 React 16.8 引入以来,Hooks 不仅简化了状态逻辑的复用,还让函数式组件拥有了类组件的全部能力——甚至更强!本文将带你深入理解 React 内置 Hooks(如 useStateuseEffect)的核心机制,并重点剖析 useEffect 的依赖项(dependencies)如何工作,解答开发者最常遇到的痛点问题,比如:

  • “为什么我的 effect 没有执行?”
  • “为什么 effect 无限循环?”
  • “空依赖 [] 真的是只执行一次吗?”

🔑 什么是 React Hooks?

use 开头的函数,都是 React Hooks。

这是 React 官方对 Hooks 的定义。它们是 React 提供的一组函数式 API,允许你在不编写 class 的情况下使用 state 和其他 React 特性。相比传统的类组件,Hooks 更贴近原生 JavaScript 函数风格,代码更简洁、逻辑更清晰。


🧠 核心内置 Hooks 解析

1️⃣ useState:响应式状态管理

jsx
编辑
const [num, setNum] = useState(0);
  • useState 返回一个状态变量和一个更新函数。

  • 初始化支持传入函数(用于复杂计算):

    jsx
    编辑
    const [num, setNum] = useState(() => {
      const a = 1 + 2;
      const b = 2 + 3;
      return a + b; // 同步计算,只执行一次
    });
    

❗ 注意:useState 的初始化函数必须是同步的
不能直接传入异步函数(如 async () => await fetchData()),因为 React 要求初始状态必须是确定且同步可得的。

✅ 那么,如何在组件挂载时发起异步请求并设置初始状态?

答案是:不要在 useState 中处理异步,而是在 useEffect 中处理!

jsx
编辑
const [data, setData] = useState(null);

useEffect(() => {
  queryData().then(res => setData(res));
}, []); // 模拟 onMounted

2️⃣ useEffect:处理副作用(Side Effects)与依赖项详解

组件本应是“纯函数”:输入 props,输出 JSX。
但现实世界充满“副作用”:API 请求、定时器、订阅等 —— 这些都由 useEffect 管理。

useEffect 的签名如下:

js
编辑
useEffect(effectFn, dependencies?)

其中 第二个参数 dependencies(依赖项数组) 决定了 effect 何时重新执行。


📌 三种典型用法与依赖项行为
场景写法执行时机类比 Vue
挂载时执行一次useEffect(() => { ... }, [])仅组件首次渲染后执行onMounted
依赖变化时执行useEffect(() => { ... }, [a, b])当 a 或 b 改变时执行watch([a, b])
每次渲染都执行useEffect(() => { ... })每次组件 re-render 后都执行onMounted + onUpdated

💡 关键原则:effect 中用到的所有外部变量(props、state、函数等),都必须出现在依赖数组中!


🔍 依赖项是如何比较的?

React 使用 Object.is 对依赖项进行浅比较(shallow comparison)

  • 基本类型(number, string, boolean):值相等即相同。
  • 引用类型(对象、数组、函数):引用地址相同才算相等

这意味着:

jsx
编辑
// 危险!每次渲染都会创建新对象,导致 effect 无限执行
useEffect(() => {
  console.log(config);
}, [{ id: 1 }]); // ❌ 每次都是新对象!

// 正确做法:提升到组件外,或用 useMemo/useCallback 缓存
const config = useMemo(() => ({ id: 1 }), []);
useEffect(() => {
  console.log(config);
}, [config]); // ✅

⚠️ 常见陷阱与解决方案
❌ 陷阱1:遗漏依赖 → 闭包过期(Stale Closure)
jsx
编辑
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // 总是打印 0!
    }, 1000);
    return () => clearInterval(id);
  }, []); // ❌ 忘记把 count 加入依赖

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

原因:count 在 effect 创建时被捕获为 0,后续更新不会影响闭包内的值。

修复方式

  • 方法一:将 count 加入依赖(但会导致定时器频繁重建)
  • 方法二:使用 函数式更新 或 ref 获取最新值
jsx
编辑
const countRef = useRef(count);
useEffect(() => {
  countRef.current = count;
}, [count]);

useEffect(() => {
  const id = setInterval(() => {
    console.log(countRef.current); // 始终是最新的
  }, 1000);
  return () => clearInterval(id);
}, []);
❌ 陷阱2:依赖包含函数 → 无限循环
jsx
编辑
function App() {
  const [data, setData] = useState([]);

  function fetchData() {
    // ...
  }

  useEffect(() => {
    fetchData();
  }, [fetchData]); // ❌ 每次渲染 fetchData 都是新函数!
}

解决方案:用 useCallback 缓存函数

jsx
编辑
const fetchData = useCallback(async () => {
  const res = await api.get();
  setData(res);
}, []); // 无外部依赖

useEffect(() => {
  fetchData();
}, [fetchData]); // ✅ 现在 fetchData 引用稳定

🧹 清理函数(Cleanup Function)

useEffect 可以返回一个函数,用于清理上一次 effect 的副作用

jsx
编辑
useEffect(() => {
  const timer = setInterval(() => { /* ... */ }, 1000);

  // 清理函数
  return () => {
    clearInterval(timer); // 组件卸载 or 下次 effect 执行前调用
  };
}, [deps]);

清理函数会在以下时机执行:

  • 组件卸载时(unmount)
  • 下一次 effect 即将执行前(如果依赖变化)

这保证了副作用不会“残留”,避免内存泄漏或无效操作。


🛠 自定义 Hooks:逻辑复用的新范式

React 鼓励你将通用逻辑封装成自定义 Hook:

jsx
编辑
// hooks/useFetch.js
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let isCancelled = false; // 防止组件卸载后 setState

    fetch(url)
      .then(res => res.json())
      .then(json => {
        if (!isCancelled) setData(json);
      })
      .finally(() => {
        if (!isCancelled) setLoading(false);
      });

    return () => {
      isCancelled = true; // 清理标志
    };
  }, [url]);

  return { data, loading };
}

✨ 自定义 Hooks 让逻辑复用变得像调用函数一样简单,彻底告别 HOC 和 Render Props 的嵌套地狱。


✅ 最佳实践总结

场景推荐做法
初始化异步数据useState(null) + useEffect(..., [])
依赖函数/对象用 useCallback / useMemo 缓存
避免 stale closure使用 ref 同步最新 state,或正确声明依赖
定时器/订阅务必在 cleanup 中清除
依赖项不确定启用 eslint-plugin-react-hooks 插件自动检查

🔧 强烈建议在项目中配置 ESLint 规则:

json
编辑
{
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

它会自动提醒你遗漏或多余的依赖!


🌟 结语:拥抱函数式思维,写出更优雅的 React

React Hooks 不仅仅是一组新 API,它代表了一种更函数式、更声明式的编程哲学。通过 useState 管理状态,useEffect 处理副作用(并正确使用依赖项),再结合自定义 Hooks 实现逻辑抽象,你将能构建出高内聚、低耦合、易测试的现代 React 应用。

记住:状态是组件的灵魂,副作用是与现实世界的桥梁,而 Hooks,正是连接二者的魔法纽带。


🚀 现在就开始重构你的类组件吧! 你会发现,函数式组件 + Hooks 的组合,不仅代码更少,逻辑也更清晰。