Container & Presentational 模式:React 中的关注点分离
Source: Dev.to
容器‑展示(Container‑Presentational)模式:在 React 中实现关注点分离
在构建大型 React 应用时,代码的组织方式直接影响可维护性、可测试性以及团队协作的效率。容器‑展示模式(有时也称为“智能‑哑组件”模式)是一种帮助我们把业务逻辑与UI 渲染分开的约定。下面我们来详细了解它的概念、实现方式以及使用场景。
目录
模式概述
容器‑展示模式的核心思想是:
- 容器组件(Container):负责获取数据、处理状态、执行副作用(如 API 调用)以及将回调函数传递给子组件。它们通常是 有状态(stateful)的,且与 Redux、Context 或自定义 Hook 紧密结合。
- 展示组件(Presentational):只负责 UI 的渲染。它们通过
props接收数据和回调,不自行管理业务逻辑。展示组件通常是 无状态(stateless)的函数组件,易于复用和单元测试。
这种划分让我们可以:
- 关注点分离:业务逻辑与 UI 互不干扰。
- 提升可测试性:展示组件可以在不依赖外部状态的情况下进行快照或行为测试。
- 增强复用性:同一个展示组件可以在不同容器中使用,只需提供不同的
props即可。
容器组件 vs. 展示组件
| 特征 | 容器组件 | 展示组件 |
|---|---|---|
| 职责 | 数据获取、状态管理、业务逻辑 | UI 渲染、布局 |
| 是否有状态 | 通常有(使用 useState、useReducer、Redux 等) | 通常无(纯函数) |
| 依赖 | 可能依赖 Redux、Context、外部 API | 只依赖 props |
| 文件位置 | src/containers/ 或 src/pages/ | src/components/ |
| 测试方式 | 需要模拟 store、网络请求等 | 只需传入不同的 props 进行快照或交互测试 |
| 示例 | UserListContainer.jsx、TodoPage.jsx | UserList.jsx、TodoItem.jsx |
实现示例
下面的例子展示了一个简单的 Todo 应用如何使用容器‑展示模式进行组织。
1. 展示组件:TodoList.jsx
import React from 'react';
import PropTypes from 'prop-types';
const TodoList = ({ todos, onToggle, onDelete }) => (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
TodoList.propTypes = {
todos: PropTypes.array.isRequired,
onToggle: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
};
export default TodoList;
说明:此组件仅负责渲染
todos列表,并通过props暴露交互回调。它不关心数据来源,也不维护任何内部状态。
2. 容器组件:TodoListContainer.jsx
import React, { useEffect, useState } from 'react';
import TodoList from './TodoList';
import { fetchTodos, toggleTodo, deleteTodo } from '../api/todos';
const TodoListContainer = () => {
const [todos, setTodos] = useState([]);
// 初始加载
useEffect(() => {
const load = async () => {
const data = await fetchTodos();
setTodos(data);
};
load();
}, []);
const handleToggle = async id => {
await toggleTodo(id);
setTodos(prev => prev.map(t => (t.id === id ? { ...t, completed: !t.completed } : t)));
};
const handleDelete = async id => {
await deleteTodo(id);
setTodos(prev => prev.filter(t => t.id !== id));
};
return <TodoList todos={todos} onToggle={handleToggle} onDelete={handleDelete} />;
};
export default TodoListContainer;
说明:容器组件负责:
- 获取数据(
fetchTodos) - 维护本地状态(
todos) - 处理业务逻辑(切换、删除)
- 把处理好的
props传递给展示组件
这样,TodoList 可以在任何需要展示 Todo 列表的地方复用,而不必关心数据来源。
3. 使用容器组件
import React from 'react';
import TodoListContainer from './components/TodoListContainer';
const App = () => (
<div>
<h1>My Todos</h1>
<TodoListContainer />
</div>
);
export default App;
优点与缺点
优点
- 关注点清晰:业务逻辑与 UI 完全分离。
- 易于测试:展示组件可以使用纯
props进行单元测试;容器组件的副作用可以通过 Mock API 或 Redux Mock Store 来验证。 - 提升复用:同一展示组件可以在不同容器或页面中使用,只需提供不同的
props。 - 团队协作友好:前端 UI 设计师可以专注于展示组件,后端或全栈开发者负责容器层的业务实现。
缺点
- 文件数量增加:每个功能页面往往需要两个文件(容器 + 展示),导致项目结构看起来更繁杂。
- 过度抽象:在非常小的组件中使用该模式可能显得“过度工程”。
- 层级嵌套:深层嵌套的容器可能导致
props传递链过长,虽然可以使用 Context 或 Redux 来缓解。
何时使用?
| 场景 | 推荐程度 |
|---|---|
| 大型或中型项目,业务逻辑复杂,需要多人协作 | ✅ 强烈推荐 |
| 需要高度复用的 UI 组件(如表格、表单、卡片) | ✅ 推荐 |
| 仅有几行 UI 的小组件,不涉及状态或副作用 | ❌ 可直接使用函数组件,无需拆分 |
| 使用 Redux / MobX / React‑Query 等全局状态管理 | ✅ 容器组件可以负责数据订阅,展示组件保持纯粹 |
结语
容器‑展示模式是一种 “把关注点放在正确的地方” 的实践。通过把 “做什么”(业务逻辑)和 “怎么展示”(UI)分离,我们可以:
- 编写更易读、易维护的代码;
- 为 UI 组件提供更好的可测试性和复用性;
- 在团队协作时让不同角色的开发者各司其职。
当然,任何模式都不是银弹。关键是根据项目规模、团队结构以及实际需求来决定是否采用。希望本文能帮助你在 React 项目中更好地组织代码,享受关注点分离带来的好处!
Introduction
如果你已经写 React 一段时间了,可能已经创建过“上帝组件”。它负责获取数据、管理数十个状态变量、处理验证、进行一些无关的计算,同时还渲染 UI。结果就是一个纠结、难以维护的组件。
这时 Container / Presentational 模式(也称为 Smart 与 Dumb 组件)出现了。它将 UI 的外观 与 数据的获取和管理 的关注点分离开来。
模式解释
想象一家高端餐厅:
- Container(厨师 / 经理) – 在厨房工作,处理供应商(API),管理库存(state),并执行业务逻辑。它不关心摆盘。
- Presentational(服务员 / 摆盘师) – 接收已经准备好的菜肴,精美地摆盘,并将其端给顾客。它对菜是如何烹饪的毫不知情。
+---------------------------+
| CONTAINER COMPONENT |
| (Logic / Data) |
+---------------------------+
| 1. Fetches data (API) |
| 2. Manages state |
| 3. Defines handlers |
+-----------+---------------+
|
(Passes data via props)
|
v
+---------------------------+
| PRESENTATIONAL COMPONENT |
| (UI / Rendering) |
+---------------------------+
| 1. Receives props |
| 2. Renders JSX / CSS |
| 3. Looks fabulous |
+---------------------------+
经典示例:加密货币列表
展示组件(“漂亮”组件)
// CryptoList.jsx
import React from 'react';
const CryptoList = ({ coins, isLoading, onRefresh }) => {
if (isLoading) {
return Loading your digital gold...;
}
return (
🚀 去月球!
{coins.map((coin) => (
**{coin.name}**: ${coin.price.toFixed(2)}
))}
刷新价格
export default CryptoList;
纯 UI:通过 props 接收数据并渲染。不了解数据来源。
容器组件(“智能”组件)
// CryptoListContainer.jsx
import React, { useState, useEffect } from 'react';
import CryptoList from './CryptoList';
const CryptoListContainer = () => {
const [coins, setCoins] = useState([]);
const [loading, setLoading] = useState(true);
const fetchCoinData = async () => {
setLoading(true);
// Simulating an API call
setTimeout(() => {
setCoins([
{ id: 1, name: 'Bitcoin', price: 45000 },
{ id: 2, name: 'Ethereum', price: 3200 },
{ id: 3, name: 'Doge', price: 0.12 },
]);
setLoading(false);
}, 1000);
};
useEffect(() => {
fetchCoinData();
}, []);
// No JSX here – just passing data down
return (
);
};
export default CryptoListContainer;
仅逻辑:处理数据获取、状态和副作用,然后将结果交给展示组件。
Benefits
可重用性
相同的 CryptoList UI 可以显示来自任何来源的数据(例如,“收藏” 与 “全部币种”),而无需触及样式逻辑。
关注点分离
- 样式错误 → 编辑表现层文件。
- 数据加载问题 → 编辑容器层文件。
无需在混合了大量逻辑的数百行代码中滚动查找。
设计师/开发者和谐
设计师可以安全地在表现层文件(HTML/CSS)中工作,而不会导致无限渲染循环,开发者则专注于容器中的数据处理。
Hook 变体
自从 React Hooks 出现后,严格的基于类的“容器”模式已经不再常见。现在你可以保持组件为函数式,同时通过提取自定义 Hook 来仍然实现关注点分离。
// useCoins.js (custom hook)
import { useState, useEffect } from 'react';
export const useCoins = () => {
const [coins, setCoins] = useState([]);
const [loading, setLoading] = useState(true);
const fetchCoinData = async () => {
setLoading(true);
// Simulated API call
setTimeout(() => {
setCoins([
{ id: 1, name: 'Bitcoin', price: 45000 },
{ id: 2, name: 'Ethereum', price: 3200 },
{ id: 3, name: 'Doge', price: 0.12 },
]);
setLoading(false);
}, 1000);
};
useEffect(() => {
fetchCoinData();
}, []);
return { coins, loading, fetchCoinData };
};
容器组件现在可以是一个薄包装器,只需使用这个 Hook 并将其结果传递给展示组件。
陷阱
过度工程
不要把一个微不足道的组件(例如,一个简单的提交按钮)拆分成单独的容器文件和展示文件。如果组件只有几行代码,额外的抽象只会增加噪音。
Prop‑Drilling 地狱
如果你发现自己在许多层级之间传递相同的 prop(例如,user={user} 传递超过 10 层),这表明应该引入 Context 或状态管理方案,而不是深层嵌套容器。
Conclusion
Container / Presentational 模式鼓励 dumb UI 和 smart logic。通过将渲染关注点与数据关注点分离,你可以获得可重用性、更清晰的职责划分,以及设计师和开发者之间更顺畅的协作。请在能够带来价值的地方使用它,避免不必要的拆分,并考虑使用 hooks 或 Context 来缓解 prop‑drilling。 🚀