# 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