人人都会AI编程

10.6 现代 React 组件复用的主流范式:自定义 Hooks 优先

更新时间:2026-07-11

在 React 早期,高阶组件(HOC)和渲染属性(Render Props)是组件逻辑复用的主要方式。但它们在实践中暴露出明显的问题:

  • HOC:会额外增加组件层级,容易出现 Props 命名冲突,类型推导困难,调试时组件树变得难以阅读。
  • Render Props:代码风格回调嵌套,容易形成“回调地狱”,阅读和重构成本较高。

随着 React 16.8 正式推出 Hooks,自定义 Hooks 已成为逻辑复用的首选方案。它直接解决了旧模式的痛点,同时也是 React 官方推荐的主流范式。

为什么自定义 Hooks 是最优解

1. 复用逻辑而非 UI

HOC 和 Render Props 本质上复用的是整个组件结构,包含可能不必要的 DOM 包裹。自定义 Hooks 只复用带有副作用的纯逻辑,不引入任何额外的组件节点,完全保持组件树的扁平化。

2. 数据来源清晰

在自定义 Hooks 中,逻辑和状态都是显式返回的,调用者可以自由决定如何将返回值绑定到 UI 上。不像 HOC 那样隐式注入 Props,也不像 Render Props 需要回调渲染。

3. 类型推导友好

TypeScript 中,自定义 Hooks 的返回值类型可以自动推导,使用起来非常自然。而 HOC 的类型编写往往需要复杂的泛型体操,Render Props 则需要对回调参数进行反复声明。

4. 无嵌套层级

多个逻辑复用场景下,多个 Hooks 可以平铺在组件内,而不会形成“组件套组件”的洋葱式嵌套。

// 旧方式:多层 HOC 嵌套
export default withAuth(withTheme(withLogger(MyComponent)));

// 新方式:扁平化调用自定义 Hooks
function MyComponent() {
  const auth = useAuth();
  const theme = useTheme();
  const logger = useLogger();
  // ...
}

自定义 Hooks 的核心设计思路

自定义 Hooks 本质上是把有状态的逻辑从一个组件中提取出来,放入可复用的函数中。它遵循 use 开头的命名约定,可以调用其他 Hooks,并返回任意值(状态、函数、ref 等)。

设计原则:

  • 单一职责:一个 Hook 只做一件事,就像组件一样保持内聚。
  • 可配置性:通过参数控制行为,通过返回值暴露控制能力。
  • 无 UI 依赖:Hook 内部不生成 JSX,调用方决定如何渲染。

典型实战场景

1. 数据请求与缓存逻辑复用

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

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) setData(data);
      })
      .catch(err => {
        if (!cancelled) setError(err);
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// 使用
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <UserCard user={user} />;
}

这个 useFetch Hook 把请求状态管理、竞态处理(通过标志位防止过期响应)封装了起来,任何组件只需传入 URL 即可获得数据与加载状态。

2. 表单状态与校验逻辑复用

function useForm(initialValues, validate) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prev => ({ ...prev, [name]: value }));
  };

  const handleBlur = (e) => {
    if (validate) {
      const validationErrors = validate(values);
      setErrors(validationErrors);
    }
  };

  const reset = () => {
    setValues(initialValues);
    setErrors({});
  };

  return { values, errors, handleChange, handleBlur, reset };
}

// 使用
function LoginForm() {
  const { values, errors, handleChange, handleBlur } = useForm(
    { email: '', password: '' },
    (vals) => {
      const errs = {};
      if (!vals.email.includes('@')) errs.email = '邮箱格式不正确';
      if (vals.password.length < 6) errs.password = '密码至少6位';
      return errs;
    }
  );

  return (
    <form>
      <input name="email" value={values.email} onChange={handleChange} onBlur={handleBlur} />
      {errors.email && <span>{errors.email}</span>}
      {/* ... */}
    </form>
  );
}

表单逻辑(状态管理、校验、重置)被彻底抽离,任何表单组件都可以复用,而不需要重复编写 useState + 回调的样板代码。

3. 浏览器能力封装(窗口尺寸、本地存储等)

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

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

function ResponsiveLayout() {
  const { width } = useWindowSize();
  return width < 768 ? <MobileView /> : <DesktopView />;
}

这种与平台 API 交互的逻辑,用自定义 Hooks 封装后,业务组件直接使用,代码干净且易于测试。

4. 业务逻辑与状态管理(购物车、权限等)

function useCart() {
  const [items, setItems] = useState([]);

  const addItem = (product) => {
    setItems(prev => {
      const existing = prev.find(item => item.id === product.id);
      if (existing) {
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      return [...prev, { ...product, quantity: 1 }];
    });
  };

  const removeItem = (productId) => {
    setItems(prev => prev.filter(item => item.id !== productId));
  };

  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);

  return { items, addItem, removeItem, total };
}

// 在多个组件中使用同一个 Hook 实例(需配合 Context 或状态提升)
function CartPage() {
  const { items, total, removeItem } = useCart();
  // ...
}

复杂的业务逻辑(如购物车操作)被封装成 useCart,可以在购物车页面、导航栏徽标、结算组件等不同地方复用相同的逻辑,只需确保它们共享同一个状态(可以通过 Context + 自建 Hook 实现全局共享)。

与高阶组件、Render Props 的对比总结

| 维度 | 自定义 Hooks | 高阶组件(HOC) | Render Props |
|------|-------------|----------------|--------------|
| 复用范围 | 有状态的逻辑 | 逻辑 + 组件结构 | 逻辑 + 渲染控制 |
| 组件树影响 | 无额外节点 | 增加包裹层 | 增加函数嵌套 |
| Props 注入 | 显式返回,调用者自由绑定 | 隐式注入,易冲突 | 通过回调参数传递 |
| TypeScript 体验 | 自然推导 | 复杂泛型 | 中等 |
| 嵌套复杂度 | 平铺使用 | 洋葱式嵌套 | 回调嵌套 |
| 学习曲线 | 低,基础 Hooks 的延伸 | 中,需要理解 Props 代理 | 中,需理解函数子组件 |
| 官方推荐 | 首推 | 旧模式,仅少量遗留场景 | 旧模式,仅少量遗留场景 |

实践中需注意的坑点

  1. 依赖闭包陷阱

Hooks 内部使用的变量(state、props)会被闭包捕获,务必正确声明 useEffect / useCallback 的依赖数组,避免读取到旧值。

  1. 避免过早抽象

自定义 Hooks 适合有明显复用逻辑的场景,不要为了抽象而抽象。如果某个逻辑只在两处使用且差别较大,先写成组件内部函数,发现了真正的共性再提取。

  1. 遵循命名约定

始终以 use 开头。这不仅可以让 ESLint 的 Hooks 规则正确检查,也让其他开发者一眼识别这是 Hook。

  1. 保持纯净性

自定义 Hooks 内部不要直接返回 JSX,也不要作为事件处理函数的工厂滥用,否则会退化为 Render Props 的另一种写法。逻辑应当清晰集中于状态、副作用和操作函数的封装。

结语

在现代 React 开发中,自定义 Hooks 是实现组件逻辑复用的第一选择。它继承了 Hooks 生态的所有优点——简洁、扁平、类型友好,也顺应了函数组件成为唯一样式的趋势。在代码评审中,如果你看到重复的 useState + useEffect 模式,几乎总是可以抽成一个自定义 Hook,使代码库更干净、更可维护。