# React Server Components trong production: mẫu thiết kế và cạm bẫy > React Server Components trong production: các mẫu đã được kiểm chứng, anti-pattern phổ biến và chiến lược debug cho ứng dụng Next.js 15 vững chắc. - Published: 2026-03-29 - Updated: 2026-05-04 - Author: Anthony Fillion-Maillet - Tags: react server components, next.js 15, rsc patterns, production, react 19 - Reading time: 9 min --- React Server Components (RSC) thay đổi căn bản cách hoạt động của render phía server trong Next.js 15, nhưng việc đưa vào production lại bộc lộ những cạm bẫy mà tài liệu chính thức không phải lúc nào cũng đề cập. Bài viết này phân tích các mẫu thiết kế hoạt động hiệu quả, các mẫu dễ vỡ và cách chẩn đoán vấn đề trước khi chúng tới production. > **Server Components và Client Components** > > Một Server Component chỉ chạy trên server và gửi không một dòng JavaScript nào tới trình duyệt. Một Client Component (đánh dấu bằng `"use client"`) chạy ở cả hai phía. Nguyên tắc: giữ Client Components càng nhỏ và càng nằm thấp trong cây càng tốt. ## Ranh giới server-client: hiểu mẫu boundary Cạm bẫy RSC phổ biến nhất liên quan đến ranh giới giữa Server và Client Components. Một khi component mang chỉ thị `"use client"`, **tất cả các con được import của nó cũng trở thành Client Components**, dù không có chỉ thị. ```tsx // ProductPage.tsx (Server Component) import { ProductDetails } from './ProductDetails' import { AddToCartButton } from './AddToCartButton' export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params const product = await getProduct(id) return (
{/* Server Component: truy cập DB trực tiếp */} {/* Client Component: tương tác cô lập */}
) } ``` ```tsx // AddToCartButton.tsx (Client Component) 'use client' import { useState } from 'react' export function AddToCartButton({ productId, price }: { productId: string; price: number }) { const [adding, setAdding] = useState(false) async function handleAdd() { setAdding(true) await fetch('/api/cart', { method: 'POST', body: JSON.stringify({ productId, quantity: 1 }), }) setAdding(false) } return ( ) } ``` Mẫu chính: truyền dữ liệu dưới dạng **props có thể serialize** từ Server Component sang Client Component. Hàm, lớp và đối tượng Date không thể vượt qua ranh giới này. ## Anti-pattern: bọc Client Component không cần thiết Lỗi thường gặp là tạo một Client Component bọc các con là Server Components, buộc toàn bộ subtree về phía client. ```tsx // PageWrapper.tsx — ANTI-PATTERN 'use client' import { useState } from 'react' // Toàn bộ nội dung con bị đẩy về phía client export function PageWrapper({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState('light') return (
{children}
) } ``` > **children như một slot** > > Giải pháp: truyền Server Components dưới dạng `children` (mẫu slot). Children được truyền dưới dạng props vẫn là Server Components ngay cả khi parent là Client Component. Đoạn mã trên hoạt động đúng miễn là `children` đến từ một parent Server Component. ```tsx // layout.tsx (Server Component) import { PageWrapper } from './PageWrapper' import { HeavyServerContent } from './HeavyServerContent' export default function Layout() { return ( {/* Vẫn là Server Component dù có wrapper client */} ) } ``` Mẫu composition này giữ được lợi ích của render server cho nội dung nặng, đồng thời cho phép tương tác ở cấp wrapper. ## Xử lý dữ liệu bất đồng bộ: mẫu fetch ngay trong component React 19 và Next.js 15 hỗ trợ `async/await` trực tiếp trong Server Components. Mẫu này đơn giản hóa việc lấy dữ liệu so với cách tiếp cận `getServerSideProps` cũ. ```tsx // UserProfile.tsx (Server Component) import { cache } from 'react' // Loại bỏ trùng lặp các lời gọi giống nhau trong cùng một lượt render const getUser = cache(async (userId: string) => { const res = await fetch(`https://api.example.com/users/${userId}`, { next: { revalidate: 3600 }, // Cache trong 1 giờ }) if (!res.ok) throw new Error('User not found') return res.json() }) export default async function UserProfile({ userId }: { userId: string }) { const user = await getUser(userId) return (

{user.name}

{user.email}

Thành viên từ {new Date(user.createdAt).toLocaleDateString('vi-VN')}

) } ``` Ba điểm quan trọng: - Hàm `cache()` của React loại bỏ trùng lặp các lời gọi giống nhau trong một lượt render server - `next: { revalidate }` kiểm soát thời gian cache phía Next.js - Lỗi trong Server Component bất đồng bộ kích hoạt `error.tsx` gần nhất ## Cạm bẫy serialize: thứ không vượt qua ranh giới Dữ liệu trao đổi giữa Server và Client Components phải có thể serialize bằng JSON. Đây là những thứ gây lỗi âm thầm hoặc crash. ```tsx // CẠM BẪY: truyền các kiểu không serialize được // Hàm — không hoạt động { /* server action */ }} /> // Thay vào đó dùng Server Action được import import { submitForm } from '@/lib/actions/form' // Đối tượng Date — không hoạt động // Chuỗi ISO — hoạt động // Map, Set, RegExp — không hoạt động // Đối tượng thuần hoặc mảng — hoạt động ``` Server Actions (các hàm đánh dấu `"use server"`) là ngoại lệ: chúng có thể được truyền dưới dạng props tới Client Component vì Next.js biến chúng thành endpoint HTTP. ## Streaming và Suspense: mẫu tải dần dần Streaming SSR với Suspense gửi HTML tới trình duyệt theo cách dần dần. Mẫu tối ưu sử dụng các Suspense boundary chi tiết quanh mỗi phần bất đồng bộ. ```tsx // DashboardPage.tsx (Server Component) import { Suspense } from 'react' import { RevenueChart } from './RevenueChart' import { RecentOrders } from './RecentOrders' import { UserStats } from './UserStats' export default function DashboardPage() { return (
}> }> }>
) } ``` Mỗi phần tải độc lập. Nếu `RevenueChart` mất 3 giây và `UserStats` mất 200 ms, phần thống kê hiện ra ngay lập tức mà không phải chờ biểu đồ. > **Suspense và SEO** > > Nội dung bên trong Suspense boundary được render ở server và đưa vào HTML ban đầu. Trình thu thập dữ liệu nhìn thấy toàn bộ nội dung. Streaming chỉ ảnh hưởng tới tốc độ chuyển dữ liệu tới trình duyệt, không ảnh hưởng tới khả năng hiển thị SEO. ## Debug ở production: lần dấu các vấn đề RSC Lỗi RSC thường khó hiểu. Ba kỹ thuật chẩn đoán hoạt động tốt ở production. **1. Phát hiện sai khớp hydration** ```tsx // debug-hydration.tsx 'use client' import { useEffect, useState } from 'react' export function HydrationDebug() { const [isClient, setIsClient] = useState(false) useEffect(() => { setIsClient(true) }, []) if (process.env.NODE_ENV !== 'development') return null return (
{isClient ? 'Client' : 'Server'}
) } ``` **2. Ghi log payload RSC** Ở Next.js 15, bật log RSC trong `next.config.ts`: ```typescript // next.config.ts const nextConfig = { logging: { fetches: { fullUrl: true, // Hiển thị URL fetch đầy đủ }, }, } export default nextConfig ``` **3. Kiểm tra kích thước payload** Payload RSC quá lớn (> 128 KB) làm giảm hiệu năng. Cần theo dõi các yêu cầu mạng có content type `text/x-component` trong DevTools. ## Mẫu nâng cao: composition với Server Actions Server Actions kết hợp với Server Components tạo ra mẫu CQRS tự nhiên: đọc trên server (RSC), ghi qua actions. ```tsx // TodoList.tsx (Server Component) import { getTodos } from '@/lib/services/todo' import { TodoForm } from './TodoForm' import { deleteTodo } from '@/lib/actions/todo' export default async function TodoList() { const todos = await getTodos() return (
    {todos.map(todo => (
  • {todo.title}
  • ))}
) } ``` ```tsx // actions/todo.ts 'use server' import { revalidatePath } from 'next/cache' import { TodoService } from '@/lib/services/todo' export async function deleteTodo(formData: FormData) { const id = formData.get('id') as string await TodoService.delete(id) revalidatePath('/todos') } ``` Lời gọi `revalidatePath` kích hoạt một lượt render Server Component mới với dữ liệu đã cập nhật, không cần tải lại toàn bộ trang. Để chuẩn bị phỏng vấn sâu hơn về các chủ đề này, có thể tham khảo module [Next.js Server Actions](/technologies/react-next/interview-questions/nextjs-server-actions) và module [Next.js Data Fetching](/technologies/react-next/interview-questions/nextjs-data-fetching) trên SharpSkill. [Tài liệu chính thức của React](https://react.dev/reference/rsc/server-components) trình bày toàn bộ đặc tả Server Components. ## Kết luận - Giữ Client Components nhỏ và cô lập ở phần thấp của cây component - Sử dụng mẫu slot (`children`) để giữ Server Components bên trong wrapper client - Luôn xác minh khả năng serialize của props khi vượt qua ranh giới server-client - Đặt các Suspense boundary chi tiết quanh mỗi phần bất đồng bộ độc lập - Theo dõi kích thước payload RSC ở production (mục tiêu < 128 KB) - Kết hợp Server Components (đọc) và Server Actions (ghi) cho mẫu CQRS tự nhiên - Sử dụng `cache()` của React để loại bỏ trùng lặp yêu cầu trong một lượt render server --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/react-next/react-server-components-patterns-pitfalls