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 方案是可行的。是否采用,主要取决于是否已经有多个视图需要同一套逻辑接口,以及这层抽象能否让组件的使用方式更清楚。
备注:内容仅供参考。
