React 設計模式:Container/Presentational 模式(容器/展示模式)
什麼是 Container/Presentational 模式?
Container/Presentational(容器/展示)模式是 React 中一種重要的設計模式,它強制將應用程序的關注點分離為兩個明確的部分:
- 容器元件(Container Components):負責處理數據邏輯、狀態管理以及與資料層的交互
- 展示元件(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>
);
優點與缺點分析
優點
-
清晰的關注點分離
- 業務邏輯與 UI 呈現完全分離
- 程式碼結構更加清晰,便於維護
-
更高的可重用性
- 展示元件可以在不同場景中重用
- 只需改變容器元件即可適應不同數據需求
-
更易於測試
- 展示元件是純函數,只需驗證給定 props 是否正確渲染
- 容器元件可以專注於業務邏輯測試
-
團隊協作更高效
- UI 設計師可以專注於展示元件
- 開發者可以專注於業務邏輯和數據處理
缺點
-
可能增加程式碼量
- 對於簡單元件,可能會感覺過度設計
- 需要創建更多文件和元件
-
Hooks 的替代方案
- 使用自定義 Hook 可以達到類似效果
- 對於小型項目可能不需要嚴格遵循此模式
-
學習曲線
- 新手可能需要時間適應這種分離方式
- 需要團隊成員都理解並遵循這種模式
現代 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} />;
};
最佳實踐建議
-
根據項目規模選擇
- 小型項目:可以靈活選擇是否嚴格分離
- 大型項目:推薦採用清晰的關注點分離
-
命名約定
- 容器元件:
<Something>Container
或<Something>Page
- 展示元件:
<Something>View
或直接<Something>
- 容器元件:
-
適度使用
- 不是所有元件都需要嚴格分離
- 對於簡單元件,可以直接合併
-
結合其他模式
- 可以與 Compound Components 模式結合
- 也可以與 Render Props 模式結合使用
結論
Container/Presentational 模式是 React 開發中一種強大的設計模式,它通過強制分離關注點來提高程式碼的可維護性和可重用性。雖然 React Hooks 提供了一些替代方案,但這種模式的核心思想——將業務邏輯與 UI 呈現分離——仍然是現代 React 開發中的重要原則。
在實際開發中,我們可以根據項目需求和團隊偏好,選擇傳統的容器/展示元件分離,或者採用自定義 Hook 的方式來實現類似的關注點分離。關鍵是保持一致的程式碼組織方式,使項目結構清晰、易於維護。