Container & Presentational 模式:React 中的关注点分离

发布: (2026年2月4日 GMT+8 13:23)
12 min read
原文: Dev.to

Source: Dev.to

容器‑展示(Container‑Presentational)模式:在 React 中实现关注点分离

在构建大型 React 应用时,代码的组织方式直接影响可维护性、可测试性以及团队协作的效率。容器‑展示模式(有时也称为“智能‑哑组件”模式)是一种帮助我们把业务逻辑UI 渲染分开的约定。下面我们来详细了解它的概念、实现方式以及使用场景。


目录

  1. 模式概述
  2. 容器组件 vs. 展示组件
  3. 实现示例
  4. 优点与缺点
  5. 何时使用?
  6. 结语

模式概述

容器‑展示模式的核心思想是:

  • 容器组件(Container):负责获取数据、处理状态、执行副作用(如 API 调用)以及将回调函数传递给子组件。它们通常是 有状态(stateful)的,且与 Redux、Context 或自定义 Hook 紧密结合。
  • 展示组件(Presentational):只负责 UI 的渲染。它们通过 props 接收数据和回调,不自行管理业务逻辑。展示组件通常是 无状态(stateless)的函数组件,易于复用和单元测试。

这种划分让我们可以:

  • 关注点分离:业务逻辑与 UI 互不干扰。
  • 提升可测试性:展示组件可以在不依赖外部状态的情况下进行快照或行为测试。
  • 增强复用性:同一个展示组件可以在不同容器中使用,只需提供不同的 props 即可。

容器组件 vs. 展示组件

特征容器组件展示组件
职责数据获取、状态管理、业务逻辑UI 渲染、布局
是否有状态通常有(使用 useStateuseReducer、Redux 等)通常无(纯函数)
依赖可能依赖 Redux、Context、外部 API只依赖 props
文件位置src/containers/src/pages/src/components/
测试方式需要模拟 store、网络请求等只需传入不同的 props 进行快照或交互测试
示例UserListContainer.jsxTodoPage.jsxUserList.jsxTodoItem.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;

说明:容器组件负责:

  1. 获取数据fetchTodos
  2. 维护本地状态todos
  3. 处理业务逻辑(切换、删除)
  4. 把处理好的 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;

优点与缺点

优点

  1. 关注点清晰:业务逻辑与 UI 完全分离。
  2. 易于测试:展示组件可以使用纯 props 进行单元测试;容器组件的副作用可以通过 Mock API 或 Redux Mock Store 来验证。
  3. 提升复用:同一展示组件可以在不同容器或页面中使用,只需提供不同的 props
  4. 团队协作友好:前端 UI 设计师可以专注于展示组件,后端或全栈开发者负责容器层的业务实现。

缺点

  1. 文件数量增加:每个功能页面往往需要两个文件(容器 + 展示),导致项目结构看起来更繁杂。
  2. 过度抽象:在非常小的组件中使用该模式可能显得“过度工程”。
  3. 层级嵌套:深层嵌套的容器可能导致 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 UIsmart logic。通过将渲染关注点与数据关注点分离,你可以获得可重用性、更清晰的职责划分,以及设计师和开发者之间更顺畅的协作。请在能够带来价值的地方使用它,避免不必要的拆分,并考虑使用 hooks 或 Context 来缓解 prop‑drilling。 🚀

Back to Blog

相关文章

阅读更多 »

理解 React 中的 useState

useState 解决了什么问题?在 React 之前,更新屏幕上的内容需要: - 找到 HTML 元素 - 手动更新它 - 确保不…