
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.
Diagnose Error #418, #423, #425 & get copy-paste code diffs
Paste Next.js or React hydration error logs to instantly diagnose server-client HTML mismatches (dates, localStorage, extension injection) and generate safe fixes.
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.
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.
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:
window, localStorage, navigator, hoặc Date.now() trong lần render ban đầu của Client ComponentMẫ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.
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.
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:
fs, crypto, net, v.v.)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:
'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 })
}
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>
)
}
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.
'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>
}
| 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 |
Một ứng dụng Next.js sử dụng App Router. Một Server Component lấy một đối tượng `Post` từ cơ sở dữ liệu, bao gồm một trường `createdAt` có kiểu `Date`. Đối tượng `Post` này sau đó được truyền dưới dạng prop cho một Client Component `PostDisplay`. Cách tiếp cận nào sau đây sẽ ngăn chặn thành công lỗi `Non-Serializable Props` trong khi vẫn đảm bảo ngày `createdAt` được hiển thị chính xác cho người dùng?
Làm sạch log AI CLI, tạo biểu thức cron, giải mã JWT và tính quyền chmod offline.

Tìm hiểu sâu về cách quản lý form trong Next.js 14 bằng Server Actions, useFormState, useFormStatus và Zod để xác thực schema, giúp bạn xây dựng các form hiện đại, nâng cao dần mà không mệt mỏi với JavaScript phía client.
Đọc tiếp
Nắm vững khi nào nên chọn Server Actions so với Route Handlers trong Next.js 15, phân tích toàn diện progressive enhancement, hành vi caching, giao thức RPC và các ranh giới bảo mật.
Đọc tiếp
Hướng dẫn kiến trúc chuyên sâu về bảo mật Next.js 15: tăng cường Server Actions chống lại CSRF, SSRF & State Injection với các ví dụ thực tế đã được kiểm chứng trong sản xuất.
Đọc tiếp