Hooks 是 React 函数组件状态管理与副作用处理的核心机制(如 useState、useEffect),但其使用存在严格规则 ------ 违反规则会导致状态混乱、生命周期执行异常等不可预测行为。
Hooks 核心使用规则(必须遵守)
React 依赖 Hooks 的调用顺序一致性来维护状态与副作用的映射关系,因此制定了两条不可违反的规则。
规则 1:只在 React 函数的最顶层调用 Hooks
Hooks 必须在函数组件或自定义 Hooks 的顶层作用域中调用,禁止在任何可能改变调用顺序的代码块中使用。
-
禁止的调用场景
嵌套函数(如事件回调、定时器 setTimeout 回调、Promise.then 回调)
条件语句(if/else、三元运算符 a ? b : c)
循环语句(for、while、map 遍历)
类组件的任何方法(如 render、handleClick,类组件不兼容 Hooks)
-
底层原因
React 会在组件首次渲染时,按 Hooks 的调用顺序建立一个 "状态链表"(如第 1 个 useState 对应链表第 1 个节点,第 2 个 useEffect 对应第 2 个节点)。若在条件 / 嵌套函数中调用 Hooks,会导致每次渲染时 Hooks 调用顺序或数量变化,React 无法匹配之前的 "状态链表",进而导致状态错乱(如读取到错误的状态值)。
-
错误与正确示例对比
js// 错误示例 1:在事件回调中调用 useState(嵌套函数) function BadComponent1() { const handleClick = () => { // ❌ 错误:Hooks 被嵌套在事件回调中,每次点击才执行,破坏调用顺序 const [count, setCount] = useState(0); setCount(1); }; return <button onClick={handleClick}>点击</button>; } // 错误示例 2:在条件语句中调用 useEffect function BadComponent2() { if (Math.random() > 0.5) { // ❌ 错误:条件为 true 时才调用,每次渲染可能改变 Hooks 调用数量/顺序 useEffect(() => { console.log("随机执行") }, []); } return <div />; } // 正确示例:在顶层调用 Hooks,条件逻辑放在 Hooks 内部 function GoodComponent() { // ✅ 正确:在组件顶层调用 useState,每次渲染顺序固定 const [count, setCount] = useState(0); const handleClick = () => { // ✅ 正确:仅更新状态(不调用新 Hooks),无顺序问题 setCount(prev => prev + 1); }; // ✅ 正确:条件逻辑放在 useEffect 内部,不影响 Hooks 调用顺序 useEffect(() => { if (count > 5) { console.log("Count 超过 5,执行副作用"); } }, [count]); // 依赖项明确,确保副作用触发时机正确 return <button onClick={handleClick}>{count}</button>; }
规则 2:只在 React 函数中调用 Hooks
Hooks 只能在两类场景中调用,禁止在普通函数、类组件中使用。
-
允许的调用场景
函数组件:直接返回 JSX 的函数(如 function MyComponent() { ... })
自定义 Hooks:以 use 开头的函数(如 useCounter、useFetch),内部可调用其他 Hooks,用于封装复用逻辑 -
错误与正确示例对比
js// 错误示例 1:在普通函数中调用 Hooks function fetchData() { // ❌ 错误:fetchData 是普通工具函数,非 React 函数 const [data, setData] = useState(null); fetch("/api") .then(res => res.json()) .then(data => setData(data)); return data; } // 错误示例 2:在类组件中调用 Hooks class ClassComponent extends React.Component { render() { // ❌ 错误:类组件有自己的状态管理(this.state)和生命周期,不兼容 Hooks const [count, setCount] = useState(0); return <div>{count}</div>; } } // 正确示例 1:在函数组件中调用 Hooks function FunctionComponent() { // ✅ 正确:函数组件是 React 认可的 Hooks 调用场景 const [count, setCount] = useState(0); return <div>函数组件状态:{count}</div>; } // 正确示例 2:在自定义 Hooks 中调用 Hooks // 自定义 Hooks 必须以 "use" 开头(React 命名约定,lint 工具依赖此识别) function useCounter(initialValue = 0) { // ✅ 正确:自定义 Hooks 内部可调用其他 Hooks const [count, setCount] = useState(initialValue); const increment = () => setCount(prev => prev + 1); const reset = () => setCount(initialValue); // 返回复用的状态与方法 return { count, increment, reset }; } // 正确示例 3:在函数组件中使用自定义 Hooks function MyComponent() { // ✅ 正确:函数组件调用自定义 Hooks,遵守规则 const { count, increment } = useCounter(5); return <button onClick={increment}>当前计数:{count}</button>; }
自定义 Hooks
自定义 Hooks 的核心价值是 "逻辑复用 " 和 "代码拆分 ",无论是公共操作的复用 ,还是业务组件内复杂逻辑的提取,都是其典型使用场景。具体来说,是否需要将逻辑放入自定义 Hooks,可从 "复用性" 和 "代码整洁度" 两个维度判断。
自定义 Hooks 的命名约定
- 自定义 Hooks 必须以 use 开头(如 useFetch 而非 fetchDataHook),原因有二:
React lint 工具识别:eslint-plugin-react-hooks 会通过命名检查 Hooks 是否被正确调用(如禁止在普通函数中调用 useXXX); - 开发者可读性:明确告知其他开发者 "此函数是自定义 Hooks,内部可能调用其他 Hooks,需遵守 Hooks 规则"。
公共操作:优先抽成自定义 Hooks(复用性优先)
当某段逻辑在 多个组件(甚至多个页面)中重复出现 时,抽成自定义 Hooks 是最佳实践。这类逻辑通常是 "通用能力",与具体业务耦合度低,复用价值高。
典型场景举例:
1. 数据请求逻辑:可抽成 useFetch
- 数据请求逻辑(带加载、错误、重试状态)多个组件都需要调用 API,且都要处理 "加载中、请求成功、请求失败" 状态,可抽成 useFetch:
js
// 公共自定义 Hooks:处理 API 请求逻辑
function useFetch(url: string) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await fetch(url);
const json = await res.json();
setData(json);
setError(null);
} catch (err) {
setError(err);
setData(null);
} finally {
setLoading(false);
}
}, [url]);
// 初始化加载
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// 组件中复用:无需重复写加载/错误逻辑
function UserList() {
const { data: users, loading, error } = useFetch("/api/users");
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error.message} />;
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
function PostList() {
const { data: posts, loading, error, refetch } = useFetch("/api/posts");
// 直接复用相同的请求逻辑...
}
2. 通用状态管理
- 通用状态管理(如弹窗、抽屉、主题切换)全局
- 通用配置 useCommonConfig
- 跨组件的状态逻辑(如 "是否显示弹窗"、"暗黑模式切换"),可抽成 useModal、useTheme 等。
- 数字格式化(国际化) useNumberFormatter
- 日期格式化(国际化) useDateFormatter
业务组件内的复杂操作:建议抽成自定义 Hooks(整洁度优先)
即使逻辑只在 单个组件中使用,但当组件内部逻辑复杂 (如包含多个状态、副作用、事件回调)时,抽成自定义 Hooks 能让组件代码更简洁,实现 "UI 渲染" 与 "业务逻辑" 的分离。
- 典型场景举例:
一个 "商品详情页" 组件,可能包含:
商品数据加载逻辑
加入购物车的逻辑(含数量增减、库存校验)
收藏 / 取消收藏的逻辑
规格选择逻辑(如颜色、尺寸切换) - 若所有逻辑都写在组件内,会导致组件冗长(数百行代码),难以维护。此时可按功能拆分多个自定义 Hooks:
js
// 商品详情页组件(只关注 UI 渲染)
function ProductDetail({ productId }) {
// 拆分逻辑到自定义 Hooks,组件只关心"用什么数据"和"调用什么方法"
const { product, loading } = useProductData(productId); // 数据加载逻辑
const { count, setCount, canAddToCart } = useCartCount(product?.stock || 0); // 数量逻辑
const { isFavorite, toggleFavorite } = useFavorite(productId); // 收藏逻辑
const { selectedSpec, setSelectedSpec } = useSpecSelector(product?.specs || []); // 规格选择逻辑
if (loading) return <Spinner />;
return (
<div className="product-detail">
<h1>{product.name}</h1>
<SpecSelector specs={product.specs} selected={selectedSpec} onChange={setSelectedSpec} />
<CountSelector count={count} onChange={setCount} />
<button disabled={!canAddToCart} onClick={() => addToCart(productId, count, selectedSpec)}>
加入购物车
</button>
<button onClick={toggleFavorite}>{isFavorite ? "取消收藏" : "收藏"}</button>
</div>
);
}
// 拆分的自定义 Hooks(每个负责一块逻辑)
function useProductData(productId) { /* 商品数据加载逻辑 */ }
function useCartCount(stock) { /* 数量计算与库存校验逻辑 */ }
function useFavorite(productId) { /* 收藏状态管理逻辑 */ }
function useSpecSelector(specs) { /* 规格选择逻辑 */ }
拆分后,组件代码从 "逻辑 + UI 混合" 变成 "只描述 UI 与逻辑的映射关系",可读性和可维护性大幅提升。
不需要抽成自定义 Hooks 的情况
自定义 Hooks 虽好,但过度拆分 反而会增加理解成本,以下场景可不必抽取:
- 逻辑简单且单一:如组件内只有一个 useState 和一个简单事件回调(几行代码),无需拆分。
- 逻辑与 UI 强耦合:若逻辑完全依赖组件的 DOM 结构或 UI 状态(如 "点击按钮滚动到顶部" 这种与特定 UI 绑定的逻辑),拆分意义不大。
- 临时过渡逻辑:仅为了修复某个 bug 而写的临时逻辑,且未来会删除,无需抽成 Hooks。
总结:自定义 Hooks 的使用原则
- 公共逻辑(多组件复用):必须抽,减少重复代码,统一维护。
- 复杂业务逻辑(单组件内):建议抽,拆分后组件更简洁,逻辑职责更清晰。
- 简单 / 耦合性强的逻辑:不必抽,避免过度设计。
核心目标 是:让代码既易于复用,又易于理解。自定义 Hooks 是实现这一目标的重要工具,但需根据实际场景灵活使用。