React设计模式之Container/Presentational

Published on
Reading time
6 min read
Likes
Authors

引言

在现代 React 应用程序开发中,组件的设计模式直接影响着代码的可维护性、可测试性和可重用性。Container/Presentational Pattern(容器/展示模式)作为一种经典的组件设计模式,通过关注点分离的方式优化了组件结构。本文将深入探讨这种模式的实现方式、应用场景及其优势。

模式概述

Container/Presentational Pattern 的核心思想是将组件按照职责划分为两类:

容器组件(Container Components)

  • 负责数据处理和业务逻辑
  • 管理组件状态
  • 处理数据获取和更新
  • 包含副作用处理
  • 提供数据和回调函数给展示组件

展示组件(Presentational Components)

  • 负责 UI 的渲染
  • 通过 props 接收数据
  • 不包含业务逻辑
  • 专注于视觉表现
  • 可以包含自己的状态(仅限 UI 状态)

实现示例

用户资料管理实现

首先,让我们看一个用户资料管理的具体实现:

javascriptEavan.dev
// UserProfileContainer.jsx
import React, { useState, useEffect } from 'react'

const UserProfileContainer = () => {
  const [userData, setUserData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    fetchUserData()
  }, [])

  const handleUpdateProfile = async (newData) => {
    try {
      await updateUserProfile(newData)
      setUserData({ ...userData, ...newData })
    } catch (err) {
      setError('更新失败')
    }
  }

  return (
    <UserProfilePresentation
      userData={userData}
      loading={loading}
      error={error}
      onUpdateProfile={handleUpdateProfile}
    />
  )
}
javascriptEavan.dev
// UserProfilePresentation.jsx
const UserProfilePresentation = ({ userData, loading, error, onUpdateProfile }) => {
  if (loading) return <div>加载中...</div>
  if (error) return <div>错误: {error}</div>

  return (
    <div className="profile-container">
      <h2>{userData.name}</h2>
      <div className="profile-details">{/* UI 渲染逻辑 */}</div>
    </div>
  )
}

核心优势

1. 关注点分离

  • 业务逻辑与 UI 表现的清晰分离
  • 更容易理解和维护的代码结构
  • 职责单一,符合单一职责原则

2. 可重用性

  • 展示组件可以被多个容器组件复用
  • 相同的 UI 可以适配不同的数据源
  • 有利于组件库的建设

3. 可测试性

  • 展示组件可以独立测试
  • 业务逻辑可以单独进行单元测试
  • 更容易模拟数据和行为

4. 开发效率

  • 前端开发人员可以专注于 UI 实现
  • 后端开发人员可以专注于数据处理
  • 并行开发效率提升

最佳实践

1. 合理划分职责

javascriptEavan.dev
// 好的实践
const TodoListContainer = () => {
  const [todos, setTodos] = useState([])
  const handleAdd = (text) => setTodos([...todos, { text, id: Date.now() }])
  return <TodoListPresentation todos={todos} onAdd={handleAdd} />
}

// 避免的实践
const TodoList = (props) => {
  const { todos } = props
  return <div>{/* 混合了数据处理和 UI 渲染 */}</div>
}

2. 保持展示组件的纯粹性

  • 避免在展示组件中直接进行 API 调用
  • 不在展示组件中处理复杂的数据转换
  • 通过 props 传递所有必要的数据和回调

3. 状态管理策略

  • 容器组件应该是有状态的
  • 展示组件主要是无状态的
  • UI 相关的小状态可以保留在展示组件中

适用场景

  1. 复杂的数据处理
  • 需要多个 API 调用
  • 涉及复杂的状态管理
  • 需要处理多个数据源
  1. 可复用的 UI 组件
  • 通用的表单组件
  • 数据展示组件
  • 可配置的UI组件
  1. 大型应用程序
  • 需要清晰的代码组织
  • 团队协作开发
  • 需要严格的代码规范

注意事项

  1. 避免过度使用
  • 简单组件不需要强行拆分
  • 考虑实际开发和维护成本
  1. 保持灵活性
  • 根据实际需求调整模式
  • 不要过分追求模式的纯粹性
  1. 性能考虑
  • 合理使用 React.memo
  • 避免不必要的组件重渲染

结构图


graph TB
  subgraph "Container Component"
      A[Container Component]
      B1[State Management]
      B2[Data Fetching]
      B3[Business Logic]
      B4[Event Handlers]

      A --> B1
      A --> B2
      A --> B3
      A --> B4
  end

  subgraph "Data Flow"
      C1[Props] --> D[Presentational Component]
      C2[Callbacks] --> D
  end

  subgraph "Presentational Component"
      D --> E1[UI Elements]
      D --> E2[Styling]
      D --> E3[User Interface Logic]
  end

  B1 --> C1
  B4 --> C2

  subgraph "External"
      API[API/Backend] --> B2
      E3 --> |UI Events| B4
  end

classDef container fill:#e1f5fe,stroke:#01579b
classDef presentation fill:#f3e5f5,stroke:#4a148c
classDef data fill:#f1f8e9,stroke:#33691e
classDef external fill:#fff3e0,stroke:#e65100

class A,B1,B2,B3,B4 container
class D,E1,E2,E3 presentation
class C1,C2 data
class API external