人人都会AI编程

封装原则与命名规范

更新时间:2026-07-10

自定义 Hooks 是 React 中复用状态逻辑的核心手段,但要写出高质量的自定义 Hooks,需要遵循一定的封装原则和命名规范。

封装原则

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

好的自定义 Hook 应该聚焦于一个明确的功能,而不是成为“大杂烩”。比如 useOnlineStatus 只负责监听网络连接状态,useDebounce 只负责防抖。当逻辑开始混合多个不相关的关注点时,就应当进一步拆分。

// ❌ 一个 Hook 做了太多事情:请求数据、处理表单、管理模态框状态
function useUserProfile() {
  const [user, setUser] = useState(null);
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [formData, setFormData] = useState({});

  useEffect(() => { fetch('/api/user').then(r => r.json()).then(setUser); }, []);

  const openModal = () => setIsModalOpen(true);
  const updateForm = (field, value) => setFormData(prev => ({ ...prev, [field]: value }));

  return { user, isModalOpen, openModal, formData, updateForm };
}

✅ 拆分为独立的、职责清晰的 Hook:

  • useUser() → 获取用户数据
  • useModal() → 管理弹窗开关
  • useForm(initialValues) → 管理表单状态

2. 明确的输入输出

每个自定义 Hook 应当有清晰的参数列表和返回值。参数决定 Hook 的行为或数据来源,返回值是组件需要使用的数据或操作方法。复杂的 Hook 可以通过返回对象来组合多个值。

// 清晰的接口:接收一个 URL,返回数据、加载状态、错误信息
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 请求逻辑...
  }, [url]);

  return { data, loading, error };
}

3. 无副作用或副作用可管理

自定义 Hook 内部可以使用 useStateuseEffect 等原生 Hook。但如果 Hook 内部有副作用(例如订阅、定时器),必须确保这些副作用能被正确清理,避免内存泄漏。

function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
    // 可选的清理逻辑
  }, [title]);
}

4. 纯逻辑,不产生 UI

自定义 Hook 只负责逻辑,不返回 JSX。如果发现 Hook 里开始返回 JSX,说明你可能需要一个组件而不是 Hook。这是一个重要的边界区分。

5. 遵守 Hooks 规则

自定义 Hook 内部只能调用其他 Hook,不能包含条件判断或循环来调用 Hook(因为必须保证调用顺序)。同时,自定义 Hook 本身也可以在条件语句中被调用(因为它本身只是函数,但内部调用 Hook 的规则不变)。为了方便识别,命名以 use 开头,React 会据此进行 lint 校验。

命名规范

1. 命名前缀:必须以 use 开头

这是 React 社区约定的命名规则,也是 React 官方 linter 插件(eslint-plugin-react-hooks)检查 Hook 规则的基础。以 use 开头可以让开发者和其他工具自动识别这是一个 Hook,并能验证其内部是否合规。

// ✅ 正确
function useLocalStorage(key, initialValue) { ... }

// ❌ 错误(linter 会报错,并且不能使用其他 Hooks)
function fetchUser(id) { ... }

2. 使用动词或功能描述

Hook 名称应当清晰地表达其功能,通常是动词或功能描述的方式,方便一目了然。

常见命名模式:

  • use + 名词/状态useUseruseAuthuseTheme
  • use + 动词 + 名词useFetchDatauseToggleuseDebounce
  • use + 功能描述useWindowSizeuseLocalStorageuseOnlineStatus

3. 返回值的命名风格

返回值通常使用数组或对象两种方式,各有适用场景:

  • 数组形式:适用于返回固定的几个值,且顺序和含义明确。类似于 useState,调用方可以任意命名变量。
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}

// 使用
const [isOpen, toggleOpen] = useToggle();
  • 对象形式:适用于返回值较多或需要明确命名的场合,调用方可以按需解构。
function useFetch(url) {
  // ...
  return { data, loading, error, refetch };
}

// 使用
const { data, loading } = useFetch('/api/users');

4. 参数命名简洁明了

参数应直观体现 Hook 的配置项或数据依赖,避免无意义的缩写。

function useDebounce(value, delay = 300) { ... }
function usePagination(fetchFn, initialPage = 1) { ... }

实际封装示例

将多个命名和封装原则结合,一个标准的自定义 Hook 如下:

/**
 * 自定义 Hook:管理浏览器本地存储的值
 * @param {string} key - localStorage 中的键名
 * @param {T} initialValue - 初始值(支持函数)
 * @returns {[T, (value: T) => void]} 当前值和设置函数
 */
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      // 支持函数式更新
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue];
}

这个 Hook 符合所有原则:单一职责(专注于本地存储)、清晰的输入(key 和初始值)和输出(值和设置函数)、无 UI 产生、命名以 use 开头、功能描述明确、返回数组允许解构命名。

遵循这些封装原则和命名规范,能让你的自定义 Hooks 更具可读性、可维护性,也更容易在团队中推广复用。