🎯 React Hooks 从入门到精通:掌握现代 React 开发的核心利器(含 useEffect 依赖项深度解析)
在现代前端开发中,React Hooks 已经成为构建高效、可维护组件的标配。自 React 16.8 引入以来,Hooks 不仅简化了状态逻辑的复用,还让函数式组件拥有了类组件的全部能力——甚至更强!本文将带你深入理解 React 内置 Hooks(如 useState 和 useEffect)的核心机制,并重点剖析 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 的组合,不仅代码更少,逻辑也更清晰。