Skip to content
Home
Go back

Container và Presentational Components trong React

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:

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íPresentationalContainer
Biết data đến từ đâuKhông, chỉ nhận qua propsCó, tự fetch hoặc đọc từ store
Có state riêngChỉ state liên quan UI (ví dụ: dropdown đang mở hay đóng)Có, thường là state nghiệp vụ (data, loading, error)
Markup/JSXCó, chiếm phần lớnThường ít hoặc không có
TestDễ, chỉ cần truyền props giảKhó hơn, cần mock API/store
Tái sử dụngCao, dùng được với nhiều nguồn data khác nhauThấ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:

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


Share this post:

Previous Post
Scope trong JavaScript: Global, Function và Block
Next Post
Compound Components Pattern trong React