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 无法匹配之前的 "状态链表",进而导致状态错乱(如读取到错误的状态值)。

  • 错误与正确示例对比

    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 的命名约定

  1. 自定义 Hooks 必须以 use 开头(如 useFetch 而非 fetchDataHook),原因有二:
    React lint 工具识别:eslint-plugin-react-hooks 会通过命名检查 Hooks 是否被正确调用(如禁止在普通函数中调用 useXXX);
  2. 开发者可读性:明确告知其他开发者 "此函数是自定义 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 是实现这一目标的重要工具,但需根据实际场景灵活使用。

相关推荐
你的人类朋友2 小时前
“签名”这个概念是非对称加密独有的吗?
前端·后端·安全
西陵2 小时前
Nx带来极致的前端开发体验——任务缓存
前端·javascript·架构
10年前端老司机3 小时前
Promise 常见面试题(持续更新中)
前端·javascript
潘小安4 小时前
跟着 AI 学 (一)- shell 脚本
前端·ci/cd·vibecoding
clownAdam5 小时前
Chrome性能优化秘籍
前端·chrome·性能优化
@Kerry~5 小时前
phpstudy .htaccess 文件内容
java·开发语言·前端
@PHARAOH6 小时前
WHAT - 前端性能指标(交互和响应性能指标)
前端·交互
噢,我明白了6 小时前
前端js 常见算法面试题目详解
前端·javascript·算法
im_AMBER6 小时前
Web 开发 30
前端·笔记·后端·学习·web