Next.js 14: Gỡ lỗi ranh giới Server-Client

Table of Contents
Next.js App Router (Next.js 13.4+) kết hợp React Server Components và Client Components thành một cây duy nhất. Hầu hết thời gian, điều này hoạt động một cách vô hình — nhưng khi có lỗi, các thông báo lỗi thường khó hiểu và stack trace lại chỉ vào các thành phần nội bộ mà bạn chưa bao giờ chạm tới. Hướng dẫn này cung cấp cho bạn các kỹ thuật cụ thể để chẩn đoán nhanh chóng mọi loại lỗi ranh giới.
Hiểu về sự phân tách hai phía
Server Components chạy chỉ trên máy chủ: chúng có thể await các lệnh gọi cơ sở dữ liệu, đọc biến môi trường và import các module chỉ dành cho Node.js. Đầu ra của chúng được tuần tự hóa dưới dạng payload RSC — một định dạng nhỏ gọn, truyền tải liên tục mà không phải HTML, không phải JSON, và không phải các gói JavaScript. Client Components (được đánh dấu 'use client') được biên dịch thành JS trình duyệt và chạy trong quá trình hydration và re-render.
Ranh giới là điểm mà một Server Component render một Client Component. Bất cứ thứ gì vượt qua ranh giới đó đều phải có thể tuần tự hóa được.
Lớp lỗi 1 — Props không thể tuần tự hóa
Những gì bạn thấy:
Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server".
at stringify (...next/dist/server/app-render/app-render.js:...)
Hoặc đối với các thể hiện của lớp:
Error: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components.
Classes or null prototypes are not supported.
Nguyên nhân: Truyền một Date, Map, Set, một thể hiện của lớp, hoặc một hàm dưới dạng prop từ Server Component sang Client Component.
// ❌ Date object is not serializable
async function Page() {
const post = await db.post.findFirst()
return <PostCard publishedAt={post.createdAt} /> // Date → crash
}
// ✅ Serialize to ISO string before passing
async function Page() {
const post = await db.post.findFirst()
return <PostCard publishedAt={post.createdAt.toISOString()} />
}
Mẹo gỡ lỗi: Log JSON.stringify(yourProp) trong Server Component trước khi truyền nó. Nếu nó ném lỗi hoặc tạo ra {}, prop đó không thể tuần tự hóa được.
Lớp lỗi 2 — Không khớp Hydration
Những gì bạn thấy trong console của trình duyệt:
Error: Hydration failed because the initial UI does not match what was rendered on the server.
Warning: Expected server HTML to contain a matching <div> in <div>.
See more info here: https://nextjs.org/docs/messages/react-hydration-error
Nguyên nhân gốc: HTML được gửi từ máy chủ không khớp với cây React mà client render trong quá trình hydration. Các yếu tố kích hoạt phổ biến:
- Sử dụng
window,localStorage,navigator, hoặcDate.now()trong lần render ban đầu của Client Component - Các múi giờ khác nhau tạo ra các chuỗi ngày khác nhau giữa máy chủ và client
- Các tiện ích mở rộng của trình duyệt chèn các nút DOM trước khi hydration
Mẫu sửa lỗi — trì hoãn mã chỉ dành cho trình duyệt:
'use client'
import { useEffect, useState } from 'react'
// ❌ Crashes: window is undefined on the server
export function ViewCount() {
const stored = localStorage.getItem('views') ?? '0'
return <span>{stored} views</span>
}
// ✅ Mount-safe: reads localStorage only after hydration
export function ViewCount() {
const [views, setViews] = useState<string | null>(null)
useEffect(() => {
setViews(localStorage.getItem('views') ?? '0')
}, [])
if (views === null) return <span>— views</span>
return <span>{views} views</span>
}
Sửa lỗi không khớp ngày tháng: Luôn truyền ngày tháng dưới dạng chuỗi ISO từ máy chủ và phân tích cú pháp chúng ở phía client bằng cách sử dụng new Date(isoString). Không gọi new Date() bên trong lần render ban đầu của Client Component.
Lớp lỗi 3 — Kiểm tra Payload RSC
Trước khi Next.js thực hiện hydration, nó truyền tải payload RSC từ máy chủ. Bạn có thể kiểm tra nó ở dạng thô để hiểu những gì máy chủ đã gửi.
Mở DevTools → tab Network → lọc theo Fetch/XHR → tải lại trang. Tìm các yêu cầu đến URL trang của bạn với Accept: text/x-component. Phản hồi trông như sau:
0:["$","div",null,{"className":"prose"},["$","h1",null,{},"Hello"]]
1:{"id":"cjld2cyuq0000t3rmniod1fga","title":"Hello World","createdAt":"2026-01-01T00:00:00.000Z"}
Điều này cho bạn biết chính xác dữ liệu nào máy chủ đã tuần tự hóa và hình dạng cây nào nó đã tạo ra. Nếu một trường bị thiếu ở đây, nó có thể không được tìm nạp hoặc đã bị loại bỏ vì không thể tuần tự hóa.
Mẹo chuyên nghiệp: Trong chế độ dev của Next.js, thêm ?_rsc=1 vào URL để buộc làm mới RSC thuần túy và kiểm tra payload một cách riêng biệt.
Lớp lỗi 4 — Module máy chủ bị rò rỉ sang client
Những gì bạn thấy:
Module build failed: You're importing a component that needs "server-only"
but none of its parents are marked with "use server", nor are they a Server Component.
Hoặc tệ hơn — không có lỗi, chỉ là một lệnh gọi API Node.js âm thầm trả về undefined trong gói trình duyệt.
Gói server-only là bộ bảo vệ chính tắc:
npm install server-only
// lib/db.ts
import 'server-only' // ← throws at build time if imported in a Client Component
import { PrismaClient } from '@prisma/client'
const db = new PrismaClient()
export { db }
Bây giờ, nếu bất kỳ Client Component nào hoặc một tệp mà nó import cố gắng import lib/db.ts, quá trình build sẽ thất bại với một lỗi rõ ràng thay vì âm thầm gửi thông tin đăng nhập cơ sở dữ liệu của bạn đến trình duyệt.
Áp dụng điều này cho bất kỳ tệp nào:
- Chứa thông tin đăng nhập cơ sở dữ liệu hoặc bí mật API
- Sử dụng các thành phần tích hợp của Node.js (
fs,crypto,net, v.v.) - Gọi các dịch vụ nội bộ không được công khai
Lớp lỗi 5 — Ranh giới hàm use server
'use server' trên một hàm (không phải một tệp) tạo ra một Server Action — một hàm chạy trên máy chủ nhưng có thể được gọi từ một Client Component. Các quy tắc rất nghiêm ngặt:
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
if (!title) throw new Error('Title is required')
await db.post.create({ data: { title } })
}
Những lỗi phổ biến tại ranh giới use server:
- Trả về các giá trị không thể tuần tự hóa — cùng quy tắc với các prop RSC. Trả về các đối tượng thuần túy.
- Tin tưởng đầu vào từ client — Server Actions là các endpoint HTTP. Luôn xác thực bằng Zod:
'use server'
import { z } from 'zod'
const schema = z.object({ title: z.string().min(1).max(200) })
export async function createPost(formData: FormData) {
const parsed = schema.safeParse({ title: formData.get('title') })
if (!parsed.success) throw new Error('Invalid input')
await db.post.create({ data: parsed.data })
}
- Các lỗi không được xử lý làm hỏng action một cách âm thầm — trong React 19, sử dụng
useActionStateđể nắm bắt trạng thái lỗi:
'use client'
import { useActionState } from 'react'
import { createPost } from './actions'
export function PostForm() {
const [error, action, isPending] = useActionState(
async (_prev: string | null, formData: FormData) => {
try {
await createPost(formData)
return null
} catch (e) {
return (e as Error).message
}
},
null
)
return (
<form action={action}>
<input name="title" />
{error && <p className="text-red-500">{error}</p>}
<button disabled={isPending}>
{isPending ? 'Saving…' : 'Create Post'}
</button>
</form>
)
}
Lớp lỗi 6 — API Taint cho dữ liệu nhạy cảm
React 19 giới thiệu experimental_taintObjectReference và experimental_taintUniqueValue để ngăn chặn việc vô tình truyền dữ liệu máy chủ nhạy cảm sang Client Components:
// app/api/user/route.ts
import { experimental_taintObjectReference } from 'react'
async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } })
// Mark the whole object as tainted — passing it as a prop will throw
experimental_taintObjectReference(
'Do not pass user objects to Client Components. Serialize only the fields you need.',
user
)
return user
}
Bật trong next.config.js:
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
taint: true,
},
}
Đây là một biện pháp phòng thủ chuyên sâu — lỗi sẽ xuất hiện tại thời điểm render trong quá trình phát triển, trước khi bất kỳ dữ liệu nào vô tình bị rò rỉ vào gói client.
Chiến lược đặt 'use client'
Quyết định hiệu suất có tác động lớn nhất trong App Router là nơi bạn vẽ ranh giới client. Một lỗi phổ biến là đặt 'use client' trên một layout hoặc thành phần trang cấp cao, điều này buộc mọi thành phần con phải vào gói client.
Quy tắc: Đẩy 'use client' sâu nhất có thể đến mức độ tương tác thực sự cần.
// ❌ Forces everything into client bundle
'use client'
export default function BlogPage({ posts }) {
const [filter, setFilter] = useState('')
return (
<div>
<input onChange={(e) => setFilter(e.target.value)} />
{posts.map(p => <PostCard key={p.id} post={p} />)}
</div>
)
}
// ✅ Only the filter widget is a Client Component
// BlogPage and PostCard remain Server Components
'use client'
export function FilterInput({ onFilter }: { onFilter: (q: string) => void }) {
return <input onChange={(e) => onFilter(e.target.value)} />
}
Server Components có thể truyền các Server Components khác dưới dạng children thông qua Client Components mà không cần biến chúng thành client-side:
// This pattern works — children are still Server Components
export default function Layout({ children }: { children: React.ReactNode }) {
return <ClientShell>{children}</ClientShell>
}
Bảng tổng hợp quy trình gỡ lỗi
| Triệu chứng | Kiểm tra đầu tiên | Công cụ |
|---|---|---|
| "Functions cannot be passed" | Các loại prop vượt qua ranh giới | JSON.stringify(prop) trong Server Component |
| Không khớp Hydration | Các API chỉ dành cho trình duyệt trong lần render ban đầu | DevTools → tab React → Lỗi Hydration |
| Payload RSC sai | Những gì máy chủ thực sự đã tuần tự hóa | Tab Network → yêu cầu text/x-component |
| Module máy chủ trong trình duyệt | Thiếu import server-only | Đầu ra build → trình phân tích gói |
| Action thất bại âm thầm | Lỗi không được xử lý trong Server Action | useActionState nắm bắt lỗi |
| Dữ liệu nhạy cảm bị rò rỉ | Đối tượng được truyền cho Client Component | experimental_taintObjectReference |
Bạn cũng có thể thích
- Next.js App Router với Prisma: Thiết lập & Kết nối Pool
- Cấu trúc thư mục Next.js App Router: Các phương pháp hay nhất & Kiến trúc doanh nghiệp (2026)
- React Testing Library user-event v14: Các phương pháp hay nhất & Di chuyển fireEvent (2026)
- Tăng cường sức mạnh cho React với Rust và WebAssembly: Hướng dẫn toàn diện
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

suppressHydrationWarning trong Next.js: Hướng dẫn sử dụng an toàn & gỡ lỗi đầy đủ
Hướng dẫn toàn diện về suppressHydrationWarning trong Next.js: sử dụng an toàn & gỡ lỗi đầy đủ với các ví dụ thực tế đã được kiểm chứng.
Read more
Lỗi Hydration trong Next.js: Cách sửa "Text Content Mismatch" & Error 418 (2026)
Hướng dẫn sửa triệt để lỗi Hydration failed (#418), Text content does not match server-rendered HTML, lỗi dark mode flash next-themes và localStorage trong Next.js.
Read more
Next.js 15 Server Actions vs Route Handlers: So sánh kiến trúc chuyên sâu
Nắm vững khi nào nên chọn Server Actions so với Route Handlers trong Next.js 15, đi sâu vào progressive enhancement, hành vi caching, giao thức RPC và các ranh giới bảo mật.
Read more