人人都会AI编程

7.5 自定义 Hooks 的封装原则与原理

更新时间:2026-07-10

自定义 Hooks 是 React 组件逻辑复用的核心手段。它允许你将组件中与状态、副作用、订阅等相关的逻辑抽离成可独立使用的函数,从而实现“逻辑共享、视图分离”。本质上,自定义 Hooks 就是use 开头的普通 JavaScript 函数,内部可以调用其他内置或自定义 Hooks

7.5.1 自定义 Hooks 的工作原理

从实现机制看,自定义 Hooks 本身并不“创造”新的状态或副作用,而是对 React 内置 Hooks 的二次封装。其运行完全依赖于 React 的 Hooks 链表机制:

  1. 调用顺序归位:React 通过调用顺序来关联每个 Hook 的内部状态。自定义 Hook 在组件中被调用时,其内部使用的 useStateuseEffect 等会按顺序插入到当前组件的 Hooks 链表中。
  2. 闭包持有状态:Hook 通过闭包保持与组件 Fiber 节点的绑定,因此自定义 Hook 内部的状态更新,会触发使用该 Hook 的组件重新渲染。
  3. 完全隔离:同一个自定义 Hook 在不同组件或同一组件的不同实例中调用,其状态互相独立,因为每次调用都会按序分配新的存储单元。

因此,写自定义 Hook 就是在写一个“携带 Hook 逻辑的可复用函数”——其本身不保存任何持久化状态,状态完全寄存在调用它的组件实例中。

简单示例:封装一个计数逻辑

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  const increment = useCallback(() => setCount(c => c + 1), []);
  const decrement = useCallback(() => setCount(c => c - 1), []);
  return { count, increment, decrement };
}

// 在组件中使用
function Counter() {
  const { count, increment, decrement } = useCounter(10);
  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  );
}

useCounter 内部调用了 useStateuseCallback,React 会将这些 Hook 链接到 Counter 组件上,count 的变化会触发 Counter 重渲染。

7.5.2 封装原则

1. 以 use 开头命名

React 约定所有 Hook 函数必须以 use 开头。这不仅是让 Lint 工具(eslint-plugin-react-hooks)能够正确检测 Hook 使用规则的约束,也是让开发者一眼识别这是一个包含了 React 运行时逻辑的函数,而不是普通的工具函数。

useWindowSize
windowSizeHookgetWindowSize

2. 单一职责:一个 Hook 只做一件事

自定义 Hook 应该像组件一样遵循单一职责原则。一个 Hook 如果混合了数据请求、状态同步、DOM 操作等多种逻辑,会变得难以测试和维护。正确的做法是拆分成多个独立的 Hook,再在组件中组合使用。

// ✅ 职责清晰
function useUserData(userId) { /* 请求用户数据 */ }
function useOnlineStatus(userId) { /* 订阅在线状态 */ }
function useWindowResize(callback) { /* 监听窗口变化 */ }

// ❌ 大杂烩
function useUserEverything(userId) { /* 请求、状态、缓存、日志混在一起 */ }

3. 明确的输入输出,避免隐式依赖

自定义 Hook 的输入(参数)和输出(返回值)应该是稳定且可预测的。避免在 Hook 内部直接访问全局变量、上下文之外的模块变量,应通过参数传入。这样 Hook 成为一个纯函数化的逻辑单元,便于测试与复用。

// ✅ 明确依赖
function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

// ❌ 隐式依赖外部变量
let appTitle = 'App';
function useDocumentTitle() {
  useEffect(() => {
    document.title = appTitle; // 变更来源不明
  });
}

4. 返回值解构友好,保持接口稳定

很多人选择返回对象(便于按需解构)、数组(类似 useState)或两者的结合。需要保证返回的引用在依赖不变时应当保持稳定,避免引发不必要的重渲染。可以使用 useMemo 稳定组合对象。

function usePagination(total, pageSize) {
  const [currentPage, setCurrentPage] = useState(1);
  const totalPages = Math.ceil(total / pageSize);

  const goTo = useCallback((page) => { /* ... */ }, []);

  // 稳定对象引用,避免调用方重复 effect
  return useMemo(() => ({
    currentPage,
    totalPages,
    goTo
  }), [currentPage, totalPages, goTo]);
}

5. 处理好副作用清理与依赖声明

如果 Hook 内部使用了 useEffectaddEventListener、定时器等,必须返回清理函数。依赖数组要如实声明所有使用的外部变量,避免闭包过期或内存泄漏。

function useTimer(ms) {
  const [tick, setTick] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setTick(t => t + 1), ms);
    return () => clearInterval(id);
  }, [ms]); // ms 变化时重建定时器

  return tick;
}

6. 避免过早抽象,先写重复再提炼

业务开发中,不要一看到两个组件有相似逻辑就立即抽成 Hook。过早抽象会让 Hook 承担过多的条件判断和配置项,反而复杂化。合理做法是:允许重复出现 2-3 次后,再抽取共用逻辑,此时边界更清晰。

7. 不改变调用方的渲染行为(保持透明)

自定义 Hook 内部不应使用 React.memoshouldComponentUpdate 等影响渲染行为的技术,因为这些会干扰调用方的优化策略。Hook 只负责逻辑,渲染优化留给组件自身决定。

7.5.3 实战:封装一个完整的网络请求 Hook

function useFetch(url) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(result => {
        if (!cancelled) {
          setData(result);
          setError(null);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setData(null);
        }
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });

    return () => { cancelled = true; };
  }, [url]);

  return { data, error, loading };
}

这个 Hook 遵循了单一职责(只做数据获取)、明确的输入(url)、稳定的输出对象、处理了竞态清理。任何组件都可以直接使用它来获取数据,不必重复编写相似代码。

7.5.4 总结

自定义 Hooks 的本质是逻辑复用而非状态复用,它是 React 组合模式的延伸。遵循命名规范、单一职责、输入输出透明、副作用管理完善等原则,可以让你的代码库保持低耦合、高可测试性。在大型项目中,充分使用自定义 Hooks 能将组件层级的复杂逻辑抽丝剥茧,形成清晰的“逻辑层”,是 React 开发中最重要的技能之一。