React 自定义 Hook 封装复用逻辑

FreeGuideOnline 最新 2026-07-06

为什么需要自定义 Hook?

在开发 React 应用时,我们经常会遇到多个组件共享同一段逻辑的情况。例如,在不同的组件中都需要监听浏览器窗口大小、获取鼠标位置,或者管理表单状态。如果只是简单地复制粘贴代码,不仅会造成代码冗余,还会让后续的维护变得异常困难。

React 提供了 自定义 Hook 这一机制,让我们能够将组件逻辑提取到可重用的函数中。自定义 Hook 本质上就是一个以 use 开头的 JavaScript 函数,它内部可以调用其他 React Hook(如 useStateuseEffect 等),从而将状态逻辑与 UI 分离。

通过自定义 Hook,你可以:

  • 减少重复代码,提高开发效率
  • 让组件的关注点更单一,逻辑更清晰
  • 在团队中共享通用逻辑,建立内部“工具箱”

自定义 Hook 基础规则

在动手编写第一个自定义 Hook 之前,你只需要牢记两条规则:

  1. 命名必须以 use 开头:这是 React 识别 Hook 的约定,确保了 Hook 的调用顺序能够被 React 正确追踪。
  2. 只在最顶层调用 Hook:不要在循环、条件或嵌套函数中调用 Hook,保证每次渲染时 Hook 的调用顺序保持一致。

只要遵循这两条规则,你就可以像搭建积木一样自由地组合内置 Hook,创造出属于自己的逻辑单元。

一个最简单的例子:useToggle

让我们从一个极简的例子开始——封装切换布尔值的逻辑。这个逻辑常出现在开关、弹窗、折叠面板等场景中。

import { useState } from 'react';

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = () => {
    setValue(prev => !prev);
  };

  return [value, toggle];
}

自定义 Hook 的返回值可以任意设计,这里我们用数组返回,方便使用者进行解构重命名:

function App() {
  const [isOpen, toggleOpen] = useToggle(false);

  return (
    <div>
      <button onClick={toggleOpen}>
        {isOpen ? '关闭' : '打开'}面板
      </button>
      {isOpen && <div>这里是面板内容</div>}
    </div>
  );
}

useToggle 完全遵循了 React 的组件理念:视图只是状态的函数,而逻辑可以独立于视图存在。

实战:封装三个高频自定义 Hook

useWindowSize – 实时监听窗口尺寸

响应式布局不仅依赖 CSS,很多时候组件也需要根据窗口大小来调整行为。我们可以把 resize 事件的监听封装成一个自定义 Hook。

import { useState, useEffect } from 'react';

function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }

    window.addEventListener('resize', handleResize);

    // 在组件卸载时移除事件监听,防止内存泄漏
    return () => window.removeEventListener('resize', handleResize);
  }, []); // 空依赖数组确保只绑定一次

  return windowSize;
}

使用起来就像使用普通状态一样简单:

function ResponsiveComponent() {
  const { width, height } = useWindowSize();

  return (
    <div>
      <p>当前窗口宽度{width}px</p>
      <p>当前窗口高度{height}px</p>
      {width < 600 ? <MobileLayout /> : <DesktopLayout />}
    </div>
  );
}

useLocalStorage – 状态与本地存储同步

很多应用需要将用户偏好或临时数据保存在 localStorage 中。手写读写逻辑又琐碎又容易出错,用自定义 Hook 来一劳永逸地解决问题。

import { useState } from 'react';

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;
    }
  });

  // 封装 setValue,同时更新状态和 localStorage
  const setValue = (value) => {
    try {
      // 允许 value 是一个函数,像 useState 一样
      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];
}

现在在任何组件中持久化数据都变得轻而易举:

function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('app-theme', 'light');

  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      当前主题{theme}
      点击切换
    </button>
  );
}

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(() => {
    // 防止组件卸载后还更新状态
    let cancelled = false;

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const json = await response.json();
        if (!cancelled) {
          setData(json);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      cancelled = true;
    };
  }, [url]); // 当 url 改变时重新请求

  return { data, loading, error };
}

使用示例:

function UserList() {
  const { data, loading, error } = useFetch('/api/users');

  if (loading) return <div>加载中...</div>;
  if (error) return <div>出错了{error}</div>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

进阶技巧与最佳实践

1. 参数尽量灵活,返回值尽量稳定

设计自定义 Hook 时,考虑让它接受可选参数来调整行为。例如,useFetch 可以加入 options 对象来控制请求头、启用轮询等。返回值建议使用对象而不是数组,这样即便未来增加返回字段,调用方的代码也不会被破坏(数组解构依赖位置,对象解构依赖名字)。

2. 使用 useCallbackuseMemo 避免不必要重渲染

如果你的自定义 Hook 返回函数或计算值,请用 useCallbackuseMemo 缓存它们,防止下游组件因为引用变化而频繁重渲染。

function useToggle(initialValue) {
  const [value, setValue] = useState(initialValue);
  
  const toggle = useCallback(() => {
    setValue(prev => !prev);
  }, []);

  return [value, toggle];
}

3. 组合已有自定义 Hook

就像搭乐高,你可以基于已有的自定义 Hook 构建更强大的 Hook。例如,我们可以结合 useLocalStorageuseToggle 来实现一个持久化的开关:

function useToggleWithStorage(key, initialValue) {
  const [stored, setStored] = useLocalStorage(key, initialValue);
  // 利用已有的 useToggle 逻辑,但替换掉原生 useState
  const toggle = useCallback(() => {
    setStored(prev => !prev);
  }, [setStored]);

  return [stored, toggle];
}

4. 保持 Hook 单一职责

一个自定义 Hook 最好只专注解决一个问题。如果一个 Hook 变得过于复杂,考虑将其拆分成几个更小的 Hook,然后在组件中组合使用它们,这样可测试性和可维护性都会大大提高。

在项目中落地自定义 Hook

当你开始在项目中使用自定义 Hook 时,建议采取以下步骤:

  • 创建专门的 hooks 目录,每个 Hook 一个文件,便于管理。
  • 编写简单的文档注释,说明 Hook 的用途、参数和返回值。
  • 为关键的 Hook 编写单元测试,使用 @testing-library/react-hooks 可以轻松测试自定义 Hook。
  • 在团队内分享,鼓励大家把可复用的逻辑封装成 Hook,逐渐沉淀出一套通用逻辑层。

自定义 Hook 是 React 生态中最重要的抽象工具之一。熟练掌握它,你将能够写出高度模块化、易于维护的前端代码。从今天开始,试着把你组件中的 useStateuseEffect 组合提炼成你自己的 Hook 吧。