React 自定义 Hook 完整指南:从原理到实战应用

在 React 中,Hook 为函数组件带来了状态与生命周期管理能力。而当你在多个组件中反复写相似逻辑时,自定义 Hook 能将这部分逻辑抽象出来,提高代码复用性与可读性。下面,我们一起探索自定义 Hook 的原理、步骤、示例与实践建议。

自定义 Hook 的核心理念与作用

复用状态逻辑:当不同组件中需要共享某种状态或副作用时,自定义 Hook 可以封装这些逻辑,避免重复编写。

解耦组件与业务逻辑:组件负责界面渲染,自定义 Hook 管理内部状态、网络请求或订阅逻辑,让组件更专注展示层。

隔离副作用与结构清晰:可以把复杂的 useEffect、清理函数、状态控制等放入 Hook 中,使组件主体结构更简洁。

提升测试性:自定义 Hook 可以单独编写测试,隔离业务逻辑与组件渲染。

自定义 Hook 其实就是一个普通的函数,但它内部可以调用 React 的其他 Hook(如 useState、useEffect 等)。它本质上是“可复用的带状态逻辑单元”。

命名规范与规则

在创建自定义 Hook 时,要遵循以下规则:

函数名称必须以 use 开头,比如 useFetch、useToggle、useAuth。

Hook 内部只能调用其他 Hook,不能在条件分支、循环或嵌套函数中调用 Hook(必须遵守 Hook 的调用顺序规则)。

自定义 Hook 只封装状态逻辑,不应该渲染 UI(即不返回 JSX,而是返回数据、状态或操作函数)。

这些规范保证了 Hook 的可预测性与正确性。

自定义 Hook 的基本步骤与示例

下面是一个从最基础到稍复杂的几个示例:

示例 1:useFetch — 封装网络请求

import { useState, useEffect } from "react";

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

  useEffect(() => {
    if (!url) return;
    setLoading(true);
    setError(null);
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error("Network response was not ok");
        return res.json();
      })
      .then(json => {
        setData(json);
      })
      .catch(err => {
        setError(err);
      })
      .finally(() => {
        setLoading(false);
      });
  }, [url]);

  return { data, loading, error };
}

使用方式:

function MyComponent() {
  const { data, loading, error } = useFetch("/api/users");
  if (loading) return <p>Loading…</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <div>{JSON.stringify(data)}</div>;
}

这个 Hook 把请求逻辑、错误处理、加载状态都抽离出来,多个组件可以共享。

示例 2:usePrevious — 获取上一次的值

import { useEffect, useRef } from "react";

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

在组件中使用:

function Counter({ count }) {
  const prev = usePrevious(count);
  return <p>当前:{count}, 上一次:{prev}</p>;
}

这个 Hook 利用了 useRef 在重渲染之间保存一个“旧值”。

示例 3:useDebounce — 防抖 Hook

import { useState, useEffect } from "react";

function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebounced(value);
    }, delay);
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debounced;
}

用在输入框等场景中,可以减少频繁请求或处理。

何时应该创建自定义 Hook

创建自定义 Hook 的场景通常包括:

  • 某段状态逻辑、网络请求、订阅逻辑在多个组件中重复出现。
  • 组件过于臃肿,需要将副作用、状态处理抽离出来。
  • 想提高代码可测试性、可读性,希望将复杂逻辑封装成单元。
  • 逻辑之间有明确边界,可以形成独立职责模块。

但也要避免过度抽象:若逻辑只在一个组件中出现,或抽象后反而使接口混乱,就不一定要提取为 Hook。

最佳实践与注意事项

  • Hook 应尽量“小而专一”,一个 Hook 只处理一种职责(如网络请求、节流、订阅)。
  • 保持 Hook 的依赖声明准确。不要遗漏 useEffect 或 useMemo 的依赖项。
  • 在 Hook 中返回清晰的接口:通常是一个对象或数组,包含状态 + 操作函数。
  • 在 Hook 中处理必要的清理(如取消订阅、清除超时、取消请求)。
  • 给 Hook 添加参数控制行为(如依赖、开关、触发条件),增加灵活性。
  • 编写针对 Hook 的单元测试,将逻辑与组件渲染分离。

总结

自定义 Hook 是 React 为逻辑复用而提供的重要机制。通过将状态、异步、订阅等逻辑封装在规则明确的 Hook 中,组件变得干净、职责单一,同时降低重复代码与提高可维护性。在实际项目中,合理设计与抽象自定义 Hook,对提升代码质量与开发效率具有显著帮助。

评论