Next.js App Router: Server Components với Client Components
Trong Next.js App Router (13+), mặc định mọi component đều là Server Component. Đây là bước chuyển lớn so với Pages Router đời cũ, và cũng là phần bị hiểu sai nhiều nhất của Next.js hiện đại.
Vấn đề
Đa số dev quen với Create React App hay Pages Router cứ dán 'use client' lên đầu mọi file "cho chắc ăn". Làm vậy là phá hỏng hết ý nghĩa của Server Component: cuối cùng vẫn gửi từng đó JavaScript xuống browser như cũ, chỉ tốn thêm công thôi.
Server Component (mặc định)
Server Component chỉ render trên server. Không có tí JS nào của nó gửi xuống browser. Nó await data trực tiếp được, đụng database được, xài secret an toàn được, vì code đó không bao giờ rời khỏi server.
// app/products/page.tsx
export default async function ProductsPage() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 },
})
const products = await res.json()
return (
<ul>
{products.map((p: { id: string; name: string }) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
)
}
Không có useState, không useEffect, không event handler gì hết ở đây - cũng không dính vụ client-side fetch waterfall luôn.
Client Component ('use client')
Client Component cần cho mấy chỗ tương tác: hook, event listener, API chỉ có ở browser (window, localStorage), hay thư viện bên thứ ba đụng tới DOM.
'use client'
import { useState } from 'react'
export default function LikeButton() {
const [liked, setLiked] = useState(false)
return (
<button onClick={() => setLiked(!liked)}>
{liked ? 'Đã thích' : 'Thích'}
</button>
)
}
Directive 'use client' đánh dấu cái ranh giới - mọi thứ import bên dưới nó cũng bị gửi xuống browser luôn, nên giữ component này nhỏ gọn thôi, đẩy nó xuống tận lá của cây component.
Luồng render thật sự chạy ra sao
Mỗi request, luồng chạy như vầy:
Hai chuyện xảy ra song song ngay khi response về tới: HTML tĩnh vẽ liền (first contentful paint nhanh), và chỉ mấy "đảo" Client Component mới được hydrate với JS. Output của Server Component không bao giờ chạy lại hay hydrate trên browser.
Khi nào xài cái nào
- Server Component (mặc định): lấy data, đọc database, xài API key/secret, render UI tĩnh hoặc gần như tĩnh, giảm bundle size.
- Client Component (
'use client'):useState/useReducer,useEffect, event handler (onClick,onChange), API của browser, context provider, thư viện UI bên thứ ba đụng DOM.
Mấy Cái Bẫy Hay Gặp
- Dán
'use client'lên cả trang chỉ vì có một nút bấm tương tác - đẩy'use client'xuống đúng cái nút đó thôi, đừng dán cả trang. - Import Server Component vào trong Client Component không hoạt động như mình nghĩ đâu - truyền Server Component vào dạng
childrenhoặc prop thay vì import trực tiếp. - Fetch secret bên trong Client Component là rò rỉ nó ra bundle browser liền. Giữ API key với DB call chỉ trong Server Component thôi.
- Quên
next: { revalidate }trên mấy lệnhfetch, làm route âm thầm bị chuyển qua static rendering hoàn toàn trong khi mình lại muốn data mới.
Mặc định xài Server Component cho mọi thứ. Chỉ thêm
'use client'ở component lá nhỏ nhất thật sự cần tương tác thôi - bundle size của mình (và người dùng của mình) sẽ cảm ơn mình đó.

