[Next.js Tutorial] Server Components vs. Client Components: Khi nào chọn cái nào?

Nếu bạn đang làm việc với Next.js, đặc biệt là từ phiên bản 13 trở đi với App Router, chắc chắn bạn đã nghe đến hai khái niệm làm thay đổi cuộc chơi: Server ComponentsClient Components. Đây không chỉ là một tính năng mới, mà là một cuộc cách mạng trong cách chúng ta tư duy và xây dựng ứng dụng React.

Server Components và Client Components trong Nextjs

Bài viết này sẽ là kim chỉ nam giúp bạn không chỉ "hiểu" mà còn "làm chủ" hoàn toàn mô hình mạnh mẽ này. Hãy cùng nhau đi sâu vào từng khía cạnh nhé!

Nền tảng cốt lõi: Tại sao lại có sự thay đổi này? 💡

Trước đây, trong React, mọi component về cơ bản đều là "Client Component". Chúng được gửi đến trình duyệt, "hydrate" và trở nên tương tác. Mô hình này đơn giản nhưng có những nhược điểm:

  • Bundle size lớn: Toàn bộ code JavaScript cho một trang đều phải được tải về, kể cả những phần chỉ để hiển thị tĩnh.
  • Data waterfalls: Việc fetching dữ liệu lồng nhau ở phía client có thể tạo ra độ trễ.
  • Lộ lọt thông tin: Các API key, token có thể vô tình bị lộ trong mã nguồn phía client.

Next.js App Router ra đời để giải quyết triệt để các vấn đề này bằng cách giới thiệu một mô hình lai (hybrid), nơi bạn có thể lựa chọn component nào sẽ render ở đâu. Và đó chính là lúc Server và Client Components tỏa sáng.

Điểm cốt lõi cần nhớ: Trong App Router, tất cả các component mặc định là Server Components.

Server Components: "Nhà bếp" thầm lặng 🧑‍🍳

Hãy tưởng tượng Server Component như một người đầu bếp trong nhà bếp. Họ chuẩn bị mọi thứ, nấu nướng, bày biện món ăn một cách hoàn hảo trước khi mang ra cho thực khách. Thực khách (trình duyệt) chỉ nhận được thành phẩm cuối cùng (HTML) mà không cần biết công thức hay quá trình chế biến phức tạp bên trong.

Server Components chạy hoàn toàn trên server. Chúng render trước khi gửi đến trình duyệt và mã JavaScript của chúng không bao giờ được gửi đến client.

Đặc điểm chính:

  • Truy cập trực tiếp tài nguyên Backend: Có thể await để lấy dữ liệu từ database, gọi API nội bộ, đọc file hệ thống (fs) một cách an toàn mà không cần tạo thêm API endpoint.
  • Bảo mật cao: Giữ an toàn tuyệt đối cho các token, API keys và logic nhạy cảm vì chúng không bao giờ rời khỏi server.
  • Hiệu năng vượt trội: Giảm lượng JavaScript gửi đến client xuống mức tối thiểu, giúp tốc độ tải trang ban đầu (First Contentful Paint) nhanh hơn đáng kể.
  • Tốt cho SEO: Nội dung được render hoàn chỉnh trên server, giúp các công cụ tìm kiếm dễ dàng thu thập và lập chỉ mục.
  • Không tương tác: Không thể sử dụng các hooks như useState, useEffect, onClick hay bất kỳ API nào của trình duyệt. Chúng chỉ dùng để hiển thị UI.

Ví dụ điển hình: Một trang blog fetch bài viết

// app/blog/[slug]/page.js
// Đây là một Server Component (mặc định)

async function getPostData(slug) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    // Giữ an toàn API key trên server
    headers: { Authorization: `Bearer ${process.env.API_SECRET}` },
  })
  return res.json()
}

export default async function BlogPostPage({ params }) {
  const post = await getPostData(params.slug)

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.author}</p>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Client Components: "Gương mặt" tương tác với người dùng 🖱️

Nếu Server Component là nhà bếp, thì Client Component chính là người phục vụ tại bàn. Họ tương tác trực tiếp với khách hàng, nhận yêu cầu, thay đổi gia vị (state) và tạo ra một trải nghiệm sống động.

Để biến một component thành Client Component, bạn chỉ cần thêm chỉ thị "use client"; ở dòng đầu tiên của file.

Client Components được render trên server (SSR/SSG) sau đó được "hydrate" và chạy trên trình duyệt, cho phép chúng sử dụng state, effects và xử lý các sự kiện từ người dùng.

Đặc điểm chính:

  • Tương tác toàn diện: Có thể sử dụng useState để quản lý trạng thái, useEffect cho các side effects, và các trình xử lý sự kiện như onClick, onChange.
  • Truy cập API trình duyệt: Sử dụng được các API như window, localStorage, geolocation.
  • Tích hợp thư viện phía client: Dễ dàng làm việc với các thư viện chỉ chạy trên trình duyệt (ví dụ: các thư viện animation).

Ví dụ điển hình: Nút "Like" tương tác

// components/LikeButton.js
// Đây là một Client Component

'use client' // Chỉ thị quan trọng nhất!

import { useState } from 'react'

export default function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes)
  const [isLiked, setIsLiked] = useState(false)

  const handleClick = () => {
    const newLikes = isLiked ? likes - 1 : likes + 1
    setLikes(newLikes)
    setIsLiked(!isLiked)
  }

  return (
    <button onClick={handleClick}>
      {isLiked ? '❤️ Đã thích' : '🤍 Thích'} ({likes})
    </button>
  )
}

Khi nào dùng gì? Quy tắc vàng để lựa chọn 🤔

Đây là câu hỏi quan trọng nhất. Dưới đây là một bảng tham khảo nhanh giúp bạn quyết định:

Dùng Server Component khi bạn cần...Dùng Client Component khi bạn cần...
fetching dữ liệu.Thêm tương tác (onClick, onChange, ...).
Truy cập trực tiếp vào backend (database, file...).Sử dụng state và lifecycle (useState, useEffect).
Giữ an toàn cho các thông tin nhạy cảm.Sử dụng các API chỉ có ở trình duyệt (window, localStorage).
Giảm lượng JavaScript ở client.Sử dụng các custom hooks dựa trên state hoặc effects.
Hiển thị UI tĩnh, không cần tương tác.Tích hợp các thư viện class component.

Nguyên tắc vàng: Hãy bắt đầu với Server Components và chỉ chuyển sang Client Components ("use client";) cho các component thực sự cần đến sự tương tác ở cấp độ "lá" (leaf nodes) trong cây component của bạn.

Server và Client Components "sống chung" như thế nào? 🤝

Sức mạnh thực sự của Next.js nằm ở khả năng kết hợp mượt mà giữa hai loại component này.

Sử dụng kết hợp Server Components và Client Components trong Nextjs

1. Server Component lồng Client Component (Phổ biến nhất)

Đây là mô hình phổ biến và tự nhiên nhất. Bạn có một Server Component (ví dụ: page.js) để fetch dữ liệu và render cấu trúc chính, sau đó import và sử dụng một Client Component (ví dụ: LikeButton.js) bên trong nó.

// app/blog/[slug]/page.js (Server Component)
import LikeButton from '@/components/LikeButton'

async function getPostData(slug) {
  /* ... */
}

export default async function BlogPostPage({ params }) {
  const post = await getPostData(params.slug)

  return (
    <article>
      <h1>{post.title}</h1>
      {/* ... nội dung bài viết ... */}

      {/* Truyền dữ liệu từ Server xuống Client qua props */}
      <LikeButton initialLikes={post.likes} />
    </article>
  )
}

Lưu ý quan trọng: Dữ liệu bạn truyền từ Server Component xuống Client Component qua props phải là dữ liệu có thể tuần tự hóa (serializable). Nghĩa là không thể truyền functions, Dates, hoặc các giá trị phức tạp không chuyển đổi được thành chuỗi.

2. Client Component lồng Server Component (Kỹ thuật nâng cao)

Bạn không thể import một Server Component vào trong một Client Component. Điều này sẽ gây lỗi!

SAI:

// components/ClientComponent.js
'use client'
import ServerComponent from '@/components/ServerComponent' // LỖI!

export default function ClientComponent() {
  return (
    <div>
      <ServerComponent />
    </div>
  )
}

ĐÚNG: Sử dụng children prop. Bạn truyền Server Component như một prop children từ một Server Component cha.

// app/layout.js (Server Component)
import ClientLayout from '@/components/ClientLayout';
import ServerSideBar from '@/components/ServerSideBar';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {/* Pass ServerSideBar như một prop cho ClientLayout */}
        <ClientLayout sidebar={<ServerSideBar />}>
          {children}
        </ClientLayout>
      </body>
    </html>
  );
}

// components/ClientLayout.js (Client Component)
"use client";

export default function ClientLayout({ children, sidebar }) {
  // ... state và logic của client component
  return (
    <div className="container">
      <aside>{sidebar}</aside> {/* Render Server Component ở đây */}
      <main>{children}</main>
    </div>
  );
}

Kết luận: Lợi ích vượt trội bạn nhận được 🚀

Khi đã có kiến thức cốt lõi về các loại component trong Nextjs, bạn sẽ nhận lại được rất nhiều lợi ích:

  • Tốc độ tia chớp ⚡: Giảm thiểu JavaScript, trang web của bạn tải nhanh hơn, mang lại trải nghiệm người dùng tuyệt vời.
  • Bảo mật vững chắc 🛡️: Logic kinh doanh và dữ liệu nhạy cảm được giữ an toàn tuyệt đối trên server.
  • Trải nghiệm lập trình viên (DX) đỉnh cao 👨‍💻: Fetching dữ liệu ngay tại component cần nó, giúp code sạch sẽ, dễ hiểu và dễ bảo trì hơn.
  • SEO Tối ưu 📈: Nội dung tĩnh được render sẵn sàng cho các con bot của Google, cải thiện thứ hạng tìm kiếm.

Server Components và Client Components không phải là hai thế giới đối lập mà là hai mảnh ghép hoàn hảo tạo nên một bức tranh ứng dụng Next.js hiện đại, hiệu năng và bảo mật.

Việc hiểu rõ khi nào nên dùng loại nào và cách chúng kết hợp với nhau chính là chìa khóa để bạn khai thác tối đa sức mạnh của Next.js App Router. Hãy bắt đầu tư duy "server-first" và chỉ sử dụng "use client"; khi thực sự cần thiết.

Chúc bạn xây dựng được những ứng dụng tuyệt vời với Nextjs!

Bài viết liên quan

[Next.js Tutorial] Styling toàn tập: Lựa chọn nào là phù hợp nhất với bạn?

Hướng dẫn từng bước cách styling trong Next.js. Bài viết này bao gồm các best practices, tips tối ưu hiệu suất để giúp bạn xây dựng UI đẹp mắt và nhanh chóng.

[Next.js Tutorial] Cấu trúc thư mục và Routing cơ bản trong dự án

Làm thế nào để tổ chức dự án Next.js hiệu quả? Hướng dẫn chi tiết về cấu trúc thư mục và hệ thống routing cơ bản, giúp bạn xây dựng website Next.js nhanh chóng và chuẩn mực.

[Next.js Tutorial] Navigation và Linking: Cách sử dụng cho hiệu suất tối ưu

Bài viết này sẽ giúp bạn làm chủ Navigation và Linking trong Next.js một cách dễ dàng. Tìm hiểu chi tiết cách sử dụng Next/link và useRouter để điều hướng trang, tối ưu hiệu suất ứng dụng của bạn.

[Next.js Tutorial] Layouts: Hướng dẫn chi tiết để tối ưu giao diện Website

Bạn muốn quản lý giao diện website Next.js một cách linh hoạt và hiệu quả? Bài viết này sẽ hướng dẫn bạn cách sử dụng layouts để tăng tốc độ phát triển và cải thiện trải nghiệm người dùng.