# React Server Components ในโปรดักชัน: รูปแบบและกับดัก
> React Server Components ในโปรดักชัน: รูปแบบที่ผ่านการพิสูจน์ แอนตี้แพทเทิร์นที่พบบ่อย และกลยุทธ์การดีบักสำหรับแอป Next.js 15 ที่มั่นคง
- 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) เปลี่ยนวิธีทำงานของการเรนเดอร์ฝั่งเซิร์ฟเวอร์ใน Next.js 15 อย่างพื้นฐาน แต่การนำไปใช้ในโปรดักชันเผยให้เห็นกับดักที่เอกสารทางการอาจไม่ได้ครอบคลุมเสมอไป บทความนี้อธิบายแยกแยะรูปแบบที่ใช้งานได้ดี รูปแบบที่พังง่าย และวิธีวินิจฉัยปัญหาก่อนที่จะไปถึงโปรดักชัน
> **Server Components และ Client Components**
>
> Server Component ทำงานเฉพาะบนเซิร์ฟเวอร์และส่ง JavaScript ไปยังเบราว์เซอร์เป็นศูนย์ ส่วน Client Component (ทำเครื่องหมายด้วย `"use client"`) ทำงานทั้งสองฝั่ง กฎคือ ให้ Client Components มีขนาดเล็กที่สุดและอยู่ตำแหน่งต่ำที่สุดในต้นไม้คอมโพเนนต์
## ขอบเขตเซิร์ฟเวอร์-ไคลเอนต์: เข้าใจรูปแบบ boundary
กับดัก RSC ที่พบบ่อยที่สุดคือเรื่องขอบเขตระหว่าง Server และ Client Components เมื่อคอมโพเนนต์ใดมีไดเรกทีฟ `"use client"` แล้ว **ลูกที่ถูก import เข้ามาทั้งหมดจะกลายเป็น Client Components ตามไปด้วย** แม้จะไม่มีไดเรกทีฟก็ตาม
```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: เข้าถึง DB โดยตรง */}
{/* Client Component: ความสามารถโต้ตอบที่แยกออกมา */}
)
}
```
```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 (
)
}
```
รูปแบบสำคัญ: ส่งข้อมูลในรูป **props ที่ serialize ได้** จาก Server Component ไปยัง Client Component ฟังก์ชัน คลาส และอ็อบเจกต์ Date ไม่สามารถข้ามขอบเขตนี้ได้
## แอนตี้แพทเทิร์น: Wrapper แบบ Client Component ที่ไม่จำเป็น
ความผิดพลาดที่พบบ่อยคือสร้าง Client Component ที่ห่อหุ้มลูกที่เป็น Server Component ทำให้ทั้งสาขาย่อยถูกบังคับให้กลายเป็นฝั่งไคลเอนต์
```tsx
// PageWrapper.tsx — แอนตี้แพทเทิร์น
'use client'
import { useState } from 'react'
// เนื้อหาลูกทั้งหมดถูกย้ายไปฝั่งไคลเอนต์
export function PageWrapper({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light')
return (
{children}
)
}
```
> **children ในรูปแบบ slot**
>
> วิธีแก้: ส่ง Server Components ผ่าน `children` (รูปแบบ slot) ลูกที่ถูกส่งเป็น props จะยังเป็น Server Components แม้ว่า parent จะเป็น Client Component โค้ดข้างต้นทำงานได้อย่างถูกต้องตราบใดที่ `children` มาจาก parent ที่เป็น Server Component
```tsx
// layout.tsx (Server Component)
import { PageWrapper } from './PageWrapper'
import { HeavyServerContent } from './HeavyServerContent'
export default function Layout() {
return (
{/* ยังคงเป็น Server Component แม้มี wrapper ที่เป็นไคลเอนต์ */}
)
}
```
รูปแบบ composition นี้รักษาประโยชน์ของการเรนเดอร์ฝั่งเซิร์ฟเวอร์สำหรับเนื้อหาหนัก ในขณะที่เปิดให้มีปฏิสัมพันธ์ในระดับ wrapper
## การจัดการข้อมูลแบบ async: รูปแบบ fetch ในคอมโพเนนต์
React 19 และ Next.js 15 รองรับ `async/await` ใน Server Components โดยตรง รูปแบบนี้ทำให้การดึงข้อมูลง่ายขึ้นเมื่อเทียบกับวิธี `getServerSideProps` แบบเดิม
```tsx
// UserProfile.tsx (Server Component)
import { cache } from 'react'
// ลดการเรียกซ้ำที่เหมือนกันภายในการเรนเดอร์เดียวกัน
const getUser = cache(async (userId: string) => {
const res = await fetch(`https://api.example.com/users/${userId}`, {
next: { revalidate: 3600 }, // แคชเป็นเวลา 1 ชั่วโมง
})
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}
เป็นสมาชิกตั้งแต่ {new Date(user.createdAt).toLocaleDateString('th-TH')}
)
}
```
สามจุดสำคัญ:
- `cache()` ของ React ลดการเรียกซ้ำที่เหมือนกันในการเรนเดอร์เซิร์ฟเวอร์เดียว
- `next: { revalidate }` ควบคุมระยะเวลาแคชฝั่ง Next.js
- ข้อผิดพลาดในรูปแบบ Server Component แบบ async จะกระตุ้น `error.tsx` ที่อยู่ใกล้ที่สุด
## กับดัก serialize: สิ่งที่ข้ามขอบเขตไม่ได้
ข้อมูลที่แลกเปลี่ยนระหว่าง Server และ Client Components ต้อง serialize เป็น JSON ได้ ต่อไปนี้คือสิ่งที่ทำให้เกิดข้อผิดพลาดเงียบหรือ crash
```tsx
// กับดัก: ส่งประเภทที่ serialize ไม่ได้
// ฟังก์ชัน — ใช้ไม่ได้
{ /* server action */ }} />
// ใช้ Server Action ที่ import เข้ามาแทน
import { submitForm } from '@/lib/actions/form'
// อ็อบเจกต์ Date — ใช้ไม่ได้
// สตริง ISO — ใช้ได้
// Map, Set, RegExp — ใช้ไม่ได้
// อ็อบเจกต์ธรรมดาหรืออาเรย์ — ใช้ได้
```
Server Actions (ฟังก์ชันที่มี `"use server"`) คือข้อยกเว้น สามารถส่งเป็น props ไปยัง Client Component ได้ เพราะ Next.js แปลงให้เป็น HTTP endpoint
## Streaming และ Suspense: รูปแบบโหลดแบบค่อยเป็นค่อยไป
SSR streaming ร่วมกับ Suspense ส่ง HTML ไปยังเบราว์เซอร์แบบทยอยส่ง รูปแบบที่ดีที่สุดใช้ Suspense boundary ละเอียดล้อมรอบทุกส่วนแบบ async
```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 (
}>
}>
}>
)
}
```
แต่ละส่วนโหลดเป็นอิสระ ถ้า `RevenueChart` ใช้เวลา 3 วินาที และ `UserStats` ใช้ 200 มิลลิวินาที ส่วนสถิติจะปรากฏทันทีโดยไม่ต้องรอกราฟ
> **Suspense และ SEO**
>
> เนื้อหาภายใน Suspense boundary ถูกเรนเดอร์ฝั่งเซิร์ฟเวอร์และอยู่ใน HTML ตั้งต้น Crawler มองเห็นเนื้อหาทั้งหมด Streaming ส่งผลเฉพาะกับความเร็วในการส่งไปยังเบราว์เซอร์ ไม่ส่งผลต่อการมองเห็น SEO
## ดีบักในโปรดักชัน: ตามรอยปัญหา RSC
ข้อผิดพลาด RSC มักดูเข้าใจยาก สามเทคนิควินิจฉัยที่ใช้ได้จริงในโปรดักชัน
**1. ระบุ hydration mismatch**
```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. บันทึกล็อก payload ของ RSC**
ใน Next.js 15 เปิดใช้การล็อก RSC ใน `next.config.ts`:
```typescript
// next.config.ts
const nextConfig = {
logging: {
fetches: {
fullUrl: true, // แสดง URL fetch แบบเต็ม
},
},
}
export default nextConfig
```
**3. ตรวจสอบขนาด payload**
payload RSC ขนาดเกิน (> 128 KB) ทำให้ประสิทธิภาพลดลง ควรเฝ้าดูคำขอเครือข่ายที่มี content type `text/x-component` ใน DevTools
## รูปแบบขั้นสูง: composition กับ Server Actions
Server Actions ผสมกับ Server Components สร้างรูปแบบ CQRS ตามธรรมชาติ: อ่านข้อมูลที่เซิร์ฟเวอร์ (RSC) เขียนข้อมูลผ่าน 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')
}
```
การเรียก `revalidatePath` กระตุ้นการเรนเดอร์ Server Component ใหม่ด้วยข้อมูลที่อัปเดต โดยไม่ต้องโหลดหน้าใหม่ทั้งหมด
หากต้องการเตรียมสัมภาษณ์ในหัวข้อเหล่านี้อย่างลึกซึ้งยิ่งขึ้น ขอแนะนำให้ดูโมดูล [Next.js Server Actions](/technologies/react-next/interview-questions/nextjs-server-actions) และโมดูล [Next.js Data Fetching](/technologies/react-next/interview-questions/nextjs-data-fetching) บน SharpSkill [เอกสารทางการของ React](https://react.dev/reference/rsc/server-components) ครอบคลุมข้อกำหนด Server Components ทั้งหมด
## สรุป
- ให้ Client Components มีขนาดเล็กและแยกอยู่ที่ฐานล่างของต้นไม้คอมโพเนนต์
- ใช้รูปแบบ slot (`children`) เพื่อรักษา Server Components ภายใน wrapper ที่เป็นไคลเอนต์
- ตรวจสอบเสมอว่า props ที่ข้ามขอบเขตเซิร์ฟเวอร์-ไคลเอนต์สามารถ serialize ได้
- วาง Suspense boundary ละเอียดรอบทุกส่วน async ที่เป็นอิสระ
- ตรวจสอบขนาด payload RSC ในโปรดักชัน (เป้าหมาย < 128 KB)
- รวม Server Components (อ่าน) และ Server Actions (เขียน) เพื่อรูปแบบ CQRS ตามธรรมชาติ
- ใช้ `cache()` ของ React เพื่อลดการเรียกซ้ำของคำขอภายในการเรนเดอร์เซิร์ฟเวอร์เดียว
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/th/blog/react-next/react-server-components-patterns-pitfalls