PAYATHON 2026

React 的 Container/Presentational 模式可以用 HOC 实现吗?

数据小许

可以把 Container 写成 HOC。Container/Presentational 描述的是组件之间的分工,HOC 则用于复用和组合组件逻辑,两者可以一起使用。

Container 固定渲染某个 Presentational 组件也可以实现关注点分离。是否需要让同一份 Container 逻辑适配多个视图,取决于实际的复用需求。

为什么示例中的 Container 通常绑定一个视图?

问题中观察到的结构通常类似这样:

function ProductContainer() {
  const products = useProducts();
  return <ProductList products={products} />;
}

useProducts() 是表示数据获取逻辑的示意名称,并非 React 内置 API。

这个 Container 负责把商品数据交给 ProductList,本身是一个具体的组合组件。这样的拆分有几个好处:

  • ProductList 可以接收来自其他地方的数据,不必了解数据如何获取。
  • 数据获取、状态处理和视图渲染可以分别维护。
  • Container 可以承担某个页面特有的数据转换和交互协调。

关注点分离不要求每一层都能独立适配任意组件。有时复用视图就足够了,让 Container 保持具体也更容易阅读。

如何用 HOC 复用 Container 逻辑?

当多个视图需要相同的数据处理或交互逻辑时,可以让 HOC 接收视图组件,返回一个提供这些能力的 Container。

下面以列表过滤为例。假设每个条目都有唯一的 id 和字符串类型的 name,数据通过 items 传入。HOC 负责过滤状态和数据处理,视图负责显示。

import { useState } from 'react';

function withFilteredItems(View) {
  function FilteredItemsContainer({ items = [], ...rest }) {
    const [query, setQuery] = useState('');
    const keyword = query.trim().toLowerCase();
    const filteredItems = items.filter((item) =>
      item.name.toLowerCase().includes(keyword)
    );

    return (
      <View
        {...rest}
        items={filteredItems}
        query={query}
        onQueryChange={setQuery}
      />
    );
  }

  FilteredItemsContainer.displayName =
    `withFilteredItems(${View.displayName || View.name || 'Component'})`;

  return FilteredItemsContainer;
}

function ItemList({ items, query, onQueryChange }) {
  return (
    <section>
      <input
        aria-label="过滤列表"
        value={query}
        onChange={(event) => onQueryChange(event.target.value)}
      />
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </section>
  );
}

function ItemSummary({ items, query, onQueryChange }) {
  return (
    <section>
      <input
        aria-label="过滤统计"
        value={query}
        onChange={(event) => onQueryChange(event.target.value)}
      />
      <p>匹配数量:{items.length}</p>
    </section>
  );
}

export const FilterableList = withFilteredItems(ItemList);
export const FilterableSummary = withFilteredItems(ItemSummary);

同一个 HOC 可以组合列表视图和统计视图。只要调用方提供所需的 props,ItemList 和 ItemSummary 也可以直接使用。

示例没有引入网络请求。如果实际的 Container 还负责获取数据,可以在其中处理请求,并按约定向视图传入 loading、error、items 等 props。请求细节取决于项目现有的数据层。

HOC 能否让两者完全解耦?

HOC 可以移除 Container 对某个具体视图组件的固定依赖,但两者仍然需要约定数据和交互接口。

上面的视图需要使用三个 props:

  • items:当前过滤后的数据。
  • query:输入框显示的过滤条件。
  • onQueryChange:更新过滤条件的回调。

如果另一个视图要求分页数据、树形节点或不同的回调语义,就需要调整接口或增加适配层。HOC 的复用范围取决于接口约定是否合适,包装本身无法保证组件可以任意组合。

另外,复用逻辑不等于共享状态。分别挂载 FilterableList 和 FilterableSummary 时,它们各自拥有独立的 query。如果两个视图需要同步过滤条件,应把状态提升到共同父组件,或使用项目已有的共享状态机制。

如何选择合适的组织方式?

只有一个页面使用某套业务逻辑时,直接让 Container 渲染具体视图通常就够了。等多个视图需要同样的能力时,再提取公共逻辑也来得及。

如果希望给多个组件注入一组稳定的 props,可以选择 HOC。创建包装组件的操作应放在模块作用域,例如示例中的 withFilteredItems(ItemList),以免在另一个组件每次渲染时重新创建组件类型。注入的 prop 名称也应明确,避免与调用方传入的同名 props 意外冲突。

如果函数组件只需要复用状态和数据处理过程,也可以提取自定义 Hook,由各个组件调用并自行组织视图。需要由调用方决定渲染结构时,还可以使用 render prop。

你的 HOC 方案是可行的。是否采用,主要取决于是否已经有多个视图需要同一套逻辑接口,以及这层抽象能否让组件的使用方式更清楚。

备注:内容仅供参考。