React 設計模式:Container/Presentational 模式(容器/展示模式)

Container/Presentational 模式示意圖

什麼是 Container/Presentational 模式?

Container/Presentational(容器/展示)模式是 React 中一種重要的設計模式,它強制將應用程序的關注點分離為兩個明確的部分:

  1. 容器元件(Container Components):負責處理數據邏輯、狀態管理以及與資料層的交互
  2. 展示元件(Presentational Components):專注於 UI 的呈現,不包含業務邏輯

這種分離使得程式碼更加模塊化、可維護性更高,並且更容易進行測試。

為什麼需要這種模式?

在 React 應用開發中,我們經常會遇到元件變得越來越複雜的情況。一個元件可能同時負責:

  • 從 API 獲取數據
  • 管理本地狀態
  • 處理用戶交互
  • 渲染 UI

這導致元件變得難以理解、測試和維護。Container/Presentational 模式通過強制分離關注點來解決這個問題。

基本實現方式

1. 容器元件(Container)

容器元件負責所有與數據和邏輯相關的工作:

import React, { useState, useEffect } from "react";
import UserList from "./UserList";
 
const UserContainer = () => {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    const fetchUsers = async () => {
      setIsLoading(true);
      try {
        const response = await fetch("https://api.example.com/users");
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setIsLoading(false);
      }
    };
 
    fetchUsers();
  }, []);
 
  const handleDeleteUser = (userId) => {
    setUsers(users.filter((user) => user.id !== userId));
  };
 
  return (
    <UserList
      users={users}
      isLoading={isLoading}
      error={error}
      onDeleteUser={handleDeleteUser}
    />
  );
};
 
export default UserContainer;

2. 展示元件(Presentational)

展示元件只負責 UI 呈現:

import React from "react";
import PropTypes from "prop-types";
 
const UserList = ({ users, isLoading, error, onDeleteUser }) => {
  if (error) {
    return <div className="error">{error}</div>;
  }
 
  if (isLoading) {
    return <div className="loading">Loading users...</div>;
  }
 
  return (
    <div className="user-list">
      <h2>User List</h2>
      {users.length === 0 ? (
        <p>No users found</p>
      ) : (
        <ul>
          {users.map((user) => (
            <li key={user.id}>
              <span>{user.name}</span>
              <button onClick={() => onDeleteUser(user.id)}>Delete</button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};
 
UserList.propTypes = {
  users: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      name: PropTypes.string.isRequired,
    })
  ).isRequired,
  isLoading: PropTypes.bool.isRequired,
  error: PropTypes.string,
  onDeleteUser: PropTypes.func.isRequired,
};
 
export default UserList;

使用 Custom Hooks 改進模式

隨著 React Hooks 的引入,我們可以使用自定義 Hook 來進一步簡化 Container/Presentational 模式:

1. 創建自定義 Hook(數據邏輯)

// useUsers.js
import { useState, useEffect } from "react";
 
const useUsers = () => {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    const fetchUsers = async () => {
      setIsLoading(true);
      try {
        const response = await fetch("https://api.example.com/users");
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setIsLoading(false);
      }
    };
 
    fetchUsers();
  }, []);
 
  const deleteUser = (userId) => {
    setUsers(users.filter((user) => user.id !== userId));
  };
 
  return { users, isLoading, error, deleteUser };
};
 
export default useUsers;

2. 簡化後的容器元件

// UsersContainer.js
import React from "react";
import useUsers from "./useUsers";
import UserList from "./UserList";
 
const UsersContainer = () => {
  const { users, isLoading, error, deleteUser } = useUsers();
 
  return (
    <UserList
      users={users}
      isLoading={isLoading}
      error={error}
      onDeleteUser={deleteUser}
    />
  );
};
 
export default UsersContainer;

實際應用場景

場景一:數據過濾

容器元件處理過濾邏輯:

const UserContainer = () => {
  const { users, isLoading, error } = useUsers();
  const [searchTerm, setSearchTerm] = useState("");
 
  const filteredUsers = users.filter((user) =>
    user.name.toLowerCase().includes(searchTerm.toLowerCase())
  );
 
  return (
    <div>
      <input
        type="text"
        placeholder="Search users..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      <UserList users={filteredUsers} isLoading={isLoading} error={error} />
    </div>
  );
};

展示元件保持不變

場景二:表單處理

容器元件處理表單狀態和提交:

const AddUserContainer = () => {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const { addUser } = useUsers();
 
  const handleSubmit = (e) => {
    e.preventDefault();
    addUser({ name, email });
    setName("");
    setEmail("");
  };
 
  return (
    <UserForm
      name={name}
      email={email}
      onNameChange={setName}
      onEmailChange={setEmail}
      onSubmit={handleSubmit}
    />
  );
};

展示元件只負責渲染表單:

const UserForm = ({ name, email, onNameChange, onEmailChange, onSubmit }) => (
  <form onSubmit={onSubmit}>
    <input
      type="text"
      value={name}
      onChange={(e) => onNameChange(e.target.value)}
      placeholder="Name"
    />
    <input
      type="email"
      value={email}
      onChange={(e) => onEmailChange(e.target.value)}
      placeholder="Email"
    />
    <button type="submit">Add User</button>
  </form>
);

優點與缺點分析

優點

  1. 清晰的關注點分離

    • 業務邏輯與 UI 呈現完全分離
    • 程式碼結構更加清晰,便於維護
  2. 更高的可重用性

    • 展示元件可以在不同場景中重用
    • 只需改變容器元件即可適應不同數據需求
  3. 更易於測試

    • 展示元件是純函數,只需驗證給定 props 是否正確渲染
    • 容器元件可以專注於業務邏輯測試
  4. 團隊協作更高效

    • UI 設計師可以專注於展示元件
    • 開發者可以專注於業務邏輯和數據處理

缺點

  1. 可能增加程式碼量

    • 對於簡單元件,可能會感覺過度設計
    • 需要創建更多文件和元件
  2. Hooks 的替代方案

    • 使用自定義 Hook 可以達到類似效果
    • 對於小型項目可能不需要嚴格遵循此模式
  3. 學習曲線

    • 新手可能需要時間適應這種分離方式
    • 需要團隊成員都理解並遵循這種模式

現代 React 中的演變

隨著 React Hooks 的普及,Container/Presentational 模式有了一些新的實現方式:

1. 使用 Context + Hooks

// UserContext.js
import { createContext, useContext, useState, useEffect } from "react";
 
const UserContext = createContext();
 
export const UserProvider = ({ children }) => {
  const [users, setUsers] = useState([]);
  // ...其他狀態和邏輯
 
  return (
    <UserContext.Provider value={{ users, addUser, deleteUser }}>
      {children}
    </UserContext.Provider>
  );
};
 
export const useUserContext = () => useContext(UserContext);

2. 直接在元件中使用自定義 Hook

const UserListPage = () => {
  const { users, isLoading, error } = useUsers();
 
  if (error) return <Error message={error} />;
  if (isLoading) return <Loading />;
 
  return <UserList users={users} />;
};

最佳實踐建議

  1. 根據項目規模選擇

    • 小型項目:可以靈活選擇是否嚴格分離
    • 大型項目:推薦採用清晰的關注點分離
  2. 命名約定

    • 容器元件:<Something>Container<Something>Page
    • 展示元件:<Something>View 或直接 <Something>
  3. 適度使用

    • 不是所有元件都需要嚴格分離
    • 對於簡單元件,可以直接合併
  4. 結合其他模式

    • 可以與 Compound Components 模式結合
    • 也可以與 Render Props 模式結合使用

結論

Container/Presentational 模式是 React 開發中一種強大的設計模式,它通過強制分離關注點來提高程式碼的可維護性和可重用性。雖然 React Hooks 提供了一些替代方案,但這種模式的核心思想——將業務邏輯與 UI 呈現分離——仍然是現代 React 開發中的重要原則。

在實際開發中,我們可以根據項目需求和團隊偏好,選擇傳統的容器/展示元件分離,或者採用自定義 Hook 的方式來實現類似的關注點分離。關鍵是保持一致的程式碼組織方式,使項目結構清晰、易於維護。