React Hooks 核心规则自定义 Hooks
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 无法匹配之前的 “状态链表”,进而导致状态错乱(如读取到错误的状态值)。 -
错误与正确示例对比
// 错误示例 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,用于封装复用逻辑 - 错误与正确示例对比
// 错误示例 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)和生命周期,不兼容 Hooksconst [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 内部可调用其他 Hooksconst [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:
// 公共自定义 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:
// 商品详情页组件(只关注 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 是实现这一目标的重要工具,但需根据实际场景灵活使用。