Mục lục
Xem mục lục
Container (Smart) và Presentational (Dumb) Component là gì
Đây là một design pattern tách component thành hai vai trò riêng biệt:
- Presentational Component (còn gọi là “Dumb” Component): chỉ lo render UI dựa trên props nhận vào, không biết data đến từ đâu, không tự fetch hay quản lý state phức tạp.
- Container Component (còn gọi là “Smart” Component): lo phần state, data fetching, xử lý logic, rồi truyền dữ liệu và các hàm callback xuống cho Presentational Component thông qua props.
Ví dụ minh họa tách một component thành hai phần
Trước khi tách, một component vừa fetch data vừa render:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
if (!user) return <p>Đang tải...</p>;
return (
<div className="profile-card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Sau khi tách thành Container và Presentational:
// Presentational: chỉ nhận props, không biết gì về fetch hay useEffect
function UserProfileView({ user, loading }) {
if (loading) return <p>Đang tải...</p>;
return (
<div className="profile-card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// Container: lo state và data fetching, không có markup riêng
function UserProfileContainer({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
return <UserProfileView user={user} loading={loading} />;
}
Tiêu chí phân biệt
| Tiêu chí | Presentational | Container |
|---|---|---|
| Biết data đến từ đâu | Không, chỉ nhận qua props | Có, tự fetch hoặc đọc từ store |
| Có state riêng | Chỉ state liên quan UI (ví dụ: dropdown đang mở hay đóng) | Có, thường là state nghiệp vụ (data, loading, error) |
| Markup/JSX | Có, chiếm phần lớn | Thường ít hoặc không có |
| Test | Dễ, chỉ cần truyền props giả | Khó hơn, cần mock API/store |
| Tái sử dụng | Cao, dùng được với nhiều nguồn data khác nhau | Thấp, gắn chặt với một nguồn data cụ thể |
Lợi ích khi tách
Separation of concerns: logic lấy dữ liệu và logic hiển thị nằm ở hai nơi riêng biệt, mỗi component chỉ có một lý do để thay đổi. Sửa cách gọi API không ảnh hưởng tới UI, và ngược lại.
Dễ test: Presentational Component chỉ cần truyền props giả để test, không cần mock API hay store:
test("hiển thị tên user", () => {
render(<UserProfileView user={{ name: "An", avatar: "", email: "" }} loading={false} />);
expect(screen.getByText("An")).toBeInTheDocument();
});
Tái sử dụng UI với nhiều nguồn data khác nhau: cùng một UserProfileView có thể được dùng bởi nhiều Container khác nhau, một Container lấy data từ REST API, một Container khác lấy từ GraphQL hoặc từ Redux store, mà không cần sửa lại phần UI.
Vì sao pattern này giảm phổ biến sau khi có Hooks
Trước khi có Hooks, Container Component là cách duy nhất để tái sử dụng logic có state giữa nhiều component, vì logic đó phải nằm trong một component class hoặc function component riêng.
Từ khi có Hooks, custom hook làm được đúng việc mà Container từng làm, nhưng không cần tạo thêm một component bọc ngoài:
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
return { user, loading };
}
Component dùng thẳng hook, không cần qua Container:
function UserProfile({ userId }) {
const { user, loading } = useUser(userId);
return <UserProfileView user={user} loading={loading} />;
}
Cách này tránh được vấn đề thường gọi là “wrapper hell”: khi nhiều Container Component lồng nhau, cây component bị kéo dài chỉ để truyền logic xuống, dù bản thân các Container đó không render markup gì đáng kể. Custom hook loại bỏ hẳn lớp bọc đó.
So sánh cách viết cũ và cách viết hiện đại
Cùng giải quyết một bài toán (tách logic fetch khỏi UI), hai cách tiếp cận khác nhau ở chỗ đặt logic:
- Container Component (cách cũ): logic nằm trong một component riêng, gắn với cây component qua việc render component con.
- Custom hook (cách hiện đại): logic nằm trong một function độc lập, gắn với component qua lời gọi hook trực tiếp, không cần thêm node nào trong component tree.
Custom hook thường được ưu tiên hơn cho các trường hợp chỉ cần chia sẻ logic state, vì gọn hơn và không ảnh hưởng tới cấu trúc cây component. Container Component vẫn có chỗ dùng khi cần chia sẻ layout hoặc kết hợp nhiều component con theo composition pattern, không chỉ đơn thuần là chia sẻ logic.
Nguyên tắc cốt lõi vẫn còn giá trị
Dù cơ chế thực hiện đã thay đổi (Container Component nhường chỗ phần lớn cho custom hook), nguyên tắc gốc của pattern này vẫn còn giá trị: tách phần UI thuần túy (nhận props, render) khỏi phần logic (state, data fetching, side effect). Component UI thuần vẫn nên dễ test, dễ tái sử dụng, không phụ thuộc vào nguồn dữ liệu cụ thể nào, bất kể logic đó được đặt trong Container Component hay trong custom hook.
Tổng kết
- Presentational Component chỉ render UI dựa trên props, không biết data đến từ đâu.
- Container Component lo state, data fetching, logic nghiệp vụ, truyền dữ liệu xuống qua props.
- Tách hai vai trò này giúp dễ test, dễ tái sử dụng UI với nhiều nguồn data khác nhau.
- Sau khi React có Hooks, custom hook thay thế phần lớn vai trò của Container Component, tránh được vấn đề wrapper hell.
- Nguyên tắc tách UI thuần khỏi logic vẫn còn giá trị, chỉ khác ở cơ chế: Container Component (component bọc) hay custom hook (function chia sẻ logic).