Server Component và Client Component trong Next.js: Những quy tắc cần nhớ để không mắc sai lầm
Sẽ rất rủi ro nếu bạn chưa nắm rõ ranh giới giữa Server Component và Client Component trong Next.js.
Bạn đang xây dựng một ứng dụng Next.js, mọi thứ trông có vẻ ổn. Rồi bỗng dưng bạn thấy màn hình trắng xóa trong vài giây mỗi khi trang load — navbar không có user, avatar trống rỗng, rồi mới hiện ra. Hoặc tệ hơn, bạn vô tình để lộ một database query xuống phía client mà không hề hay biết.
Vấn đề không nằm ở logic — mà nằm ở chỗ bạn chưa nắm rõ ranh giới giữa Server Component và Client Component trong Next.js.
Server Component và Client Component là gì?
Next.js App Router có hai loại component:
// ServerComponent.tsx — không cần khai báo gì, đây là mặc định
export default async function UserProfile() {
const user = await db.user.findUnique(...) // ✅ truy cập DB trực tiếp
return <div>{user.name}</div>
}
// ClientComponent.tsx
"use client"
export default function Dropdown() {
const [open, setOpen] = useState(false) // ✅ dùng state bình thường
return <button onClick={() => setOpen(!open)}>Menu</button>
}
Quy tắc 1: "use client" lây lan xuống, không lây ngược lên
Đây là điều dễ gây nhầm lẫn nhất. Khi bạn đặt "use client" vào một file, tất cả những gì được import bên trong file đó cũng trở thành client, dù chúng không có directive này.
// ❌ Vấn đề tiềm ẩn
"use client"
import HeavyChart from "./HeavyChart" // → trở thành client
import UserService from "./UserService" // → trở thành client, dù bạn không muốn
Ngược lại, children hoặc props được truyền từ bên ngoài vào thì không bị ảnh hưởng — chúng vẫn giữ nguyên là Server Component.
// ✅ Layout server component
export default async function Layout({ children }) {
const user = await getUser()
return <Navbar user={user}>{children}</Navbar>
// ↑ children vẫn là server, không bị Navbar "lây"
}
Quy tắc nhớ nhanh: "use client" chỉ lây qua import, không lây qua props hay children.
Quy tắc 2: Đẩy "use client" xuống càng sâu càng tốt
Một sai lầm phổ biến là đặt "use client" ở component cha cấp cao — chỉ vì cần một button có onClick. Điều này vô tình biến cả một cây component lớn thành client-side.
// ❌ Không nên — toàn bộ ProductPage thành client chỉ vì một button
"use client"
export default function ProductPage() {
const [added, setAdded] = useState(false)
return (
<div>
<ProductInfo /> {/* không cần client */}
<ReviewList /> {/* không cần client */}
<button onClick={() => setAdded(true)}>Add to cart</button>
</div>
)
}
// ✅ Nên làm — tách phần interactive ra riêng
// AddToCartButton.tsx
"use client"
export default function AddToCartButton() {
const [added, setAdded] = useState(false)
return <button onClick={() => setAdded(true)}>Add to cart</button>
}
// ProductPage.tsx — vẫn là server component
export default async function ProductPage() {
return (
<div>
<ProductInfo />
<ReviewList />
<AddToCartButton /> {/* chỉ mình nó là client */}
</div>
)
}
Quy tắc 3: Data fetching thuộc về server, interactivity thuộc về client
Đây là nguyên tắc cốt lõi nhất. Hãy nghĩ theo hướng:
Áp dụng vào Navbar — một ví dụ rất thực tế:
// ❌ Sai — fetch user bên trong client component
"use client"
export default function Navbar() {
const [user, setUser] = useState(null)
useEffect(() => {
fetch("/api/me").then(res => res.json()).then(setUser)
// Vấn đề:
// 1. Trang load xong mới fetch → navbar trống trong vài giây
// 2. Phải tạo thêm một API route chỉ để phục vụ chỗ này
// 3. Khó cache hơn
}, [])
return <nav>{user ? user.name : "..."}</nav>
}
// ✅ Đúng — server component lo data, client component lo UI
// layout.tsx (server)
export default async function Layout({ children }) {
const user = await getUser() // fetch một lần, ngay trên server
const navItems = await getNavItems()
return (
<>
<Navbar user={user} items={navItems} />
{children}
</>
)
}
// Navbar.tsx (client)
"use client"
export default function Navbar({ user, items }) {
const [menuOpen, setMenuOpen] = useState(false)
// Chỉ xử lý UI, không fetch gì cả
return (
<nav>
<span>{user.name}</span>
<button onClick={() => setMenuOpen(!menuOpen)}>☰</button>
{menuOpen && <NavItems items={items} />}
</nav>
)
}
Quy tắc 4: Dùng console.log để xác nhận component đang chạy ở đâu
Vì Next.js trừu tượng hóa nhiều thứ, đôi khi bạn không chắc component mình đang ở đâu. Thủ thuật đơn giản:
export default function SomeComponent() {
console.log("Tôi đang chạy ở đây") // log này xuất hiện ở đâu?
return <div>...</div>
}
Do & Don’t
✅ Nên
❌ Không nên
Data fetching
Trong Server Component, truyền xuống qua props
useEffect + fetch bên trong Client Component
"use client"
Đặt ở component leaf (button, dropdown, modal)
Đặt ở component cha cấp cao chỉ vì một interaction nhỏ
DB / secret
Chỉ dùng trong Server Component
Import vào Client Component dù “chỉ để dùng thử”
Modal, Drawer
Client Component nhận children từ server
Import và fetch data bên trong modal
Kiểm tra
console.log để xác nhận vị trí render
Đoán mò rồi deploy
Tóm lại
Ranh giới giữa Server và Client Component không phức tạp — nhưng rất dễ vi phạm nếu không để ý. Ba điều cần nhớ:
Nắm được ba điều này, bạn sẽ tránh được phần lớn các lỗi phổ biến khi làm việc với Next.js App Router.
