React 19: Server Components ในระบบ Production - คู่มือฉบับสมบูรณ์
เชี่ยวชาญ React 19 Server Components ในระบบ Production สถาปัตยกรรม รูปแบบการออกแบบ Streaming Caching และการเพิ่มประสิทธิภาพสำหรับแอปพลิเคชันที่มีประสิทธิภาพสูง

Server Components ถือเป็นการพัฒนาที่สำคัญที่สุดใน React นับตั้งแต่การเปิดตัว Hooks ด้วย React 19 สถาปัตยกรรมนี้ได้เติบโตจนพร้อมสำหรับการใช้งานในระบบ Production ทำให้ Component สามารถทำงานได้โดยตรงบนเซิร์ฟเวอร์ในขณะที่ยังคงรักษาความสามารถในการโต้ตอบฝั่ง Client ไว้ได้
คู่มือนี้ถูกเขียนขึ้นโดยสมมติว่าท่านมีความคุ้นเคยกับ React และ Next.js App Router ตัวอย่างที่นำเสนอใช้ Next.js 14+ ซึ่งรองรับ React Server Components โดยค่าเริ่มต้น
ทำความเข้าใจสถาปัตยกรรม Server Components
Server Components (RSC) นำเสนอแนวคิดใหม่ โดย Component บางส่วนทำงานเฉพาะบนเซิร์ฟเวอร์ บางส่วนทำงานบน Client และทั้งสองสามารถอยู่ร่วมกันในโครงสร้าง Component เดียวกันได้ การแบ่งแยกนี้ช่วยเพิ่มประสิทธิภาพอย่างมากด้วยการลดขนาด JavaScript Bundle ที่ส่งไปยังเบราว์เซอร์
แนวคิดพื้นฐานตั้งอยู่บนความจริงที่ว่า Component จำนวนมากไม่จำเป็นต้องมีการโต้ตอบ Component ที่แสดงรายการบทความจากฐานข้อมูลสามารถทำงานได้ทั้งหมดบนเซิร์ฟเวอร์ มีเพียงองค์ประกอบที่ต้องโต้ตอบ (ปุ่ม แบบฟอร์ม แอนิเมชัน) เท่านั้นที่ต้องใช้ JavaScript ฝั่ง Client
// This component runs only on the server
// No JavaScript is sent to the client for this component
import { getArticles } from '@/lib/articles'
import ArticleCard from './ArticleCard'
import LikeButton from './LikeButton'
// async/await directly in the component
// Only possible with Server Components
export default async function ArticlesPage() {
// Direct database call (no REST API needed)
const articles = await getArticles()
return (
<main className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">Recent Articles</h1>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{articles.map((article) => (
// ArticleCard is also a Server Component
<ArticleCard key={article.id} article={article}>
{/* LikeButton is a Client Component (interactive) */}
<LikeButton articleId={article.id} />
</ArticleCard>
))}
</div>
</main>
)
}Directive "use client" ใช้สำหรับระบุอย่างชัดเจนว่า Component ใดต้องการ JavaScript บนเบราว์เซอร์
'use client'
// useState and interactive hooks require "use client"
import { useState, useTransition } from 'react'
import { likeArticle } from '@/actions/articles'
interface LikeButtonProps {
articleId: string
initialLikes?: number
}
export default function LikeButton({ articleId, initialLikes = 0 }: LikeButtonProps) {
// Local state for optimistic UI
const [likes, setLikes] = useState(initialLikes)
const [isPending, startTransition] = useTransition()
const handleLike = () => {
// Immediate optimistic update
setLikes((prev) => prev + 1)
// Server Action to persist
startTransition(async () => {
await likeArticle(articleId)
})
}
return (
<button
onClick={handleLike}
disabled={isPending}
className="flex items-center gap-2 px-3 py-1 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
>
<span>❤️</span>
<span>{likes}</span>
</button>
)
}สถาปัตยกรรมนี้ช่วยลดขนาด JavaScript Bundle ได้อย่างมาก โดยมีเพียงโค้ดของ LikeButton เท่านั้นที่ถูกส่งไปยัง Client ไม่ใช่ ArticlesPage หรือ ArticleCard
รูปแบบการประกอบ Server/Client
การประกอบระหว่าง Server และ Client Components เป็นไปตามกฎที่ชัดเจน Server Component สามารถ Import และ Render Client Component ได้ แต่ในทางกลับกันไม่สามารถทำได้โดยตรง ในการส่งเนื้อหาจากเซิร์ฟเวอร์ไปยัง Client Component รูปแบบ children คือทางออก
'use client'
import { useState, ReactNode } from 'react'
interface InteractiveWrapperProps {
children: ReactNode
expandable?: boolean
}
// Client Component that wraps server content
export function InteractiveWrapper({ children, expandable = false }: InteractiveWrapperProps) {
const [isExpanded, setIsExpanded] = useState(!expandable)
if (!expandable) {
return <div>{children}</div>
}
return (
<div className="border rounded-lg overflow-hidden">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full p-4 text-left font-medium bg-gray-50 hover:bg-gray-100"
>
{isExpanded ? '▼ Collapse' : '▶ Expand'}
</button>
{isExpanded && (
<div className="p-4">
{/* children can contain Server Components */}
{children}
</div>
)}
</div>
)
}// Server Component using the client wrapper
import { InteractiveWrapper } from '@/components/InteractiveWrapper'
import { getStats, getRecentActivity } from '@/lib/dashboard'
export default async function DashboardPage() {
// Parallel server-side requests
const [stats, activity] = await Promise.all([
getStats(),
getRecentActivity()
])
return (
<div className="space-y-6">
{/* Stats in an expandable wrapper */}
<InteractiveWrapper expandable>
{/* This content is rendered server-side then passed to client */}
<div className="grid grid-cols-3 gap-4">
<StatCard title="Users" value={stats.users} />
<StatCard title="Revenue" value={stats.revenue} />
<StatCard title="Orders" value={stats.orders} />
</div>
</InteractiveWrapper>
{/* Recent activity */}
<InteractiveWrapper>
<ActivityList activities={activity} />
</InteractiveWrapper>
</div>
)
}รูปแบบนี้ช่วยให้สามารถผสมผสานการโต้ตอบฝั่ง Client เข้ากับข้อมูลที่ Render บนเซิร์ฟเวอร์ได้โดยไม่ต้องทำซ้ำ Logic
การดึงข้อมูลและ Caching
React 19 ขยาย API fetch ดั้งเดิมโดยอัตโนมัติเพื่อเพิ่มการขจัดความซ้ำซ้อนและ Caching Request ที่เหมือนกันภายใน Render เดียวกันจะถูกเรียกใช้เพียงครั้งเดียว
การดึงข้อมูลใน Server Components ทำได้โดยตรงด้วย async/await React จัดการการขจัด Request ที่ซ้ำซ้อนโดยอัตโนมัติ
// Centralized request configuration with caching
const API_BASE = process.env.API_URL
// Request with time-based revalidation
export async function getProducts() {
const response = await fetch(`${API_BASE}/products`, {
// Revalidate every hour
next: { revalidate: 3600 }
})
if (!response.ok) {
throw new Error('Failed to fetch products')
}
return response.json()
}
// Request without cache (real-time data)
export async function getCurrentUser() {
const response = await fetch(`${API_BASE}/me`, {
// No cache, always fresh
cache: 'no-store'
})
if (!response.ok) {
return null
}
return response.json()
}
// Request with tag for targeted invalidation
export async function getProduct(id: string) {
const response = await fetch(`${API_BASE}/products/${id}`, {
next: {
tags: [`product-${id}`],
revalidate: 3600
}
})
if (!response.ok) {
throw new Error('Product not found')
}
return response.json()
}สำหรับการเข้าถึงฐานข้อมูลโดยตรง (Prisma, Drizzle) React Cache ร่วมกับ unstable_cache มอบความสามารถเดียวกัน
import { unstable_cache } from 'next/cache'
import { prisma } from '@/lib/prisma'
// Cache categories (rarely modified)
export const getCategories = unstable_cache(
async () => {
return prisma.category.findMany({
orderBy: { name: 'asc' }
})
},
['categories'], // Cache key
{
revalidate: 86400, // 24 hours
tags: ['categories']
}
)
// Cache products by category
export const getProductsByCategory = unstable_cache(
async (categoryId: string) => {
return prisma.product.findMany({
where: { categoryId },
include: { images: true },
orderBy: { createdAt: 'desc' }
})
},
['products-by-category'],
{
revalidate: 3600,
tags: ['products']
}
)
// Cache invalidation after mutation
export async function createProduct(data: ProductInput) {
const product = await prisma.product.create({ data })
// Invalidate related caches
revalidateTag('products')
return product
}พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Streaming และ Suspense เพื่อประสบการณ์ผู้ใช้ที่ดีที่สุด
Streaming ช่วยให้สามารถส่ง HTML ไปยังเบราว์เซอร์แบบค่อยเป็นค่อยไป แสดงส่วนที่พร้อมแล้วทันทีในขณะที่ส่วนอื่นยังคงโหลดอยู่ เมื่อใช้ร่วมกับ Suspense กลไกนี้ช่วยปรับปรุง Time to First Byte (TTFB) และประสบการณ์ผู้ใช้ได้อย่างเห็นได้ชัด
import { Suspense } from 'react'
import { getProduct } from '@/lib/products'
import ProductDetails from './ProductDetails'
import ProductReviews from './ProductReviews'
import RecommendedProducts from './RecommendedProducts'
import { Skeleton } from '@/components/ui/Skeleton'
interface ProductPageProps {
params: { id: string }
}
export default async function ProductPage({ params }: ProductPageProps) {
// This request blocks initial render
const product = await getProduct(params.id)
return (
<div className="container mx-auto py-8">
{/* Immediate render with product data */}
<ProductDetails product={product} />
{/* Reviews load via streaming */}
<section className="mt-12">
<h2 className="text-2xl font-bold mb-6">Customer Reviews</h2>
<Suspense fallback={<ReviewsSkeleton />}>
{/* This async component will be streamed */}
<ProductReviews productId={params.id} />
</Suspense>
</section>
{/* Recommendations too */}
<section className="mt-12">
<h2 className="text-2xl font-bold mb-6">Similar Products</h2>
<Suspense fallback={<ProductGridSkeleton count={4} />}>
<RecommendedProducts productId={params.id} />
</Suspense>
</section>
</div>
)
}
// Skeleton for reviews
function ReviewsSkeleton() {
return (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="border rounded-lg p-4">
<Skeleton className="h-4 w-32 mb-2" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
))}
</div>
)
}// Async Server Component that will be streamed
import { getProductReviews } from '@/lib/reviews'
interface ProductReviewsProps {
productId: string
}
export default async function ProductReviews({ productId }: ProductReviewsProps) {
// This request may take time
// Component will be streamed when complete
const reviews = await getProductReviews(productId)
if (reviews.length === 0) {
return (
<p className="text-gray-500 italic">
No reviews yet. Be the first to share your thoughts!
</p>
)
}
return (
<div className="space-y-4">
{reviews.map((review) => (
<article key={review.id} className="border rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="font-medium">{review.author}</span>
<span className="text-yellow-500">
{'★'.repeat(review.rating)}{'☆'.repeat(5 - review.rating)}
</span>
</div>
<p className="text-gray-700">{review.content}</p>
<time className="text-sm text-gray-500">
{new Date(review.createdAt).toLocaleDateString('en-US')}
</time>
</article>
))}
</div>
)
}เบราว์เซอร์จะได้รับโครงสร้าง HTML พร้อม Skeleton ก่อน จากนั้นเนื้อหาจริงจะถูกแทรกเข้ามาทีละส่วนผ่าน Streaming
Server Actions สำหรับ Mutations
Server Actions ช่วยให้สามารถเรียกใช้โค้ดฝั่งเซิร์ฟเวอร์จาก Client Component ได้โดยไม่ต้องสร้าง API Route วิธีการนี้ช่วยลดความซับซ้อนในการจัดการ Mutation ได้อย่างมาก
'use server'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { prisma } from '@/lib/prisma'
import { getCurrentUser } from '@/lib/auth'
// Action to add to cart
export async function addToCart(productId: string, quantity: number = 1) {
const user = await getCurrentUser()
if (!user) {
// Return structured error
return { error: 'Login required', code: 'UNAUTHORIZED' }
}
try {
// Check stock
const product = await prisma.product.findUnique({
where: { id: productId }
})
if (!product || product.stock < quantity) {
return { error: 'Insufficient stock', code: 'OUT_OF_STOCK' }
}
// Add or update cart item
await prisma.cartItem.upsert({
where: {
cartId_productId: {
cartId: user.cartId,
productId
}
},
update: {
quantity: { increment: quantity }
},
create: {
cartId: user.cartId,
productId,
quantity
}
})
// Invalidate cart page cache
revalidatePath('/cart')
return { success: true, message: 'Product added to cart' }
} catch (error) {
console.error('Add to cart error:', error)
return { error: 'An error occurred', code: 'SERVER_ERROR' }
}
}
// Action to remove from cart
export async function removeFromCart(itemId: string) {
const user = await getCurrentUser()
if (!user) {
return { error: 'Login required' }
}
await prisma.cartItem.delete({
where: { id: itemId, cart: { userId: user.id } }
})
revalidatePath('/cart')
return { success: true }
}'use client'
import { useTransition } from 'react'
import { addToCart } from '@/actions/cart'
import { toast } from '@/components/ui/toast'
interface AddToCartButtonProps {
productId: string
}
export function AddToCartButton({ productId }: AddToCartButtonProps) {
const [isPending, startTransition] = useTransition()
const handleClick = () => {
startTransition(async () => {
const result = await addToCart(productId)
if (result.error) {
toast.error(result.error)
return
}
toast.success(result.message)
})
}
return (
<button
onClick={handleClick}
disabled={isPending}
className="w-full py-3 px-6 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isPending ? (
<span className="flex items-center justify-center gap-2">
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Adding...
</span>
) : (
'Add to Cart'
)}
</button>
)
}ควรตรวจสอบความถูกต้องของข้อมูลใน Server Actions เสมอ การตรวจสอบฝั่ง Client สามารถถูกข้ามได้ ใช้ Zod หรือไลบรารีที่คล้ายกันเพื่อให้การตรวจสอบมีความน่าเชื่อถือ
การจัดการข้อผิดพลาดและ Error Boundaries
React 19 ปรับปรุงการจัดการข้อผิดพลาดใน Server Components โดย Error Boundaries ทำงานในลักษณะเดียวกับ Client Components
'use client'
// Error Boundary for /products segment
interface ErrorProps {
error: Error & { digest?: string }
reset: () => void
}
export default function ProductsError({ error, reset }: ErrorProps) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-center">
<div className="p-6 bg-red-50 rounded-lg max-w-md">
<h2 className="text-xl font-bold text-red-800 mb-2">
Loading Error
</h2>
<p className="text-red-600 mb-4">
Unable to load products. Please try again.
</p>
{/* Display digest for debugging */}
{error.digest && (
<p className="text-sm text-gray-500 mb-4">
Reference: {error.digest}
</p>
)}
<button
onClick={reset}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
Try Again
</button>
</div>
</div>
)
}// Loading UI during initial load
export default function ProductsLoading() {
return (
<div className="container mx-auto py-8">
<div className="h-8 w-48 bg-gray-200 rounded animate-pulse mb-6" />
<div className="grid grid-cols-3 gap-6">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="border rounded-lg p-4">
<div className="aspect-square bg-gray-200 rounded animate-pulse mb-4" />
<div className="h-4 bg-gray-200 rounded animate-pulse mb-2" />
<div className="h-4 w-2/3 bg-gray-200 rounded animate-pulse" />
</div>
))}
</div>
</div>
)
}สำหรับการจัดการข้อผิดพลาดที่ละเอียดยิ่งขึ้นใน Component เฉพาะ สามารถใช้ Component ErrorBoundary ได้โดยตรง
'use client'
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log error to monitoring service
console.error('ErrorBoundary caught:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-4 bg-red-50 rounded-lg">
<p className="text-red-600">An error occurred</p>
</div>
)
}
return this.props.children
}
}การเพิ่มประสิทธิภาพสำหรับ Production
เทคนิคหลายประการช่วยเพิ่มประสิทธิภาพของ Server Components ในระบบ Production
import { Suspense } from 'react'
import { headers } from 'next/headers'
// Preload critical data
export const dynamic = 'force-dynamic'
export default async function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{/* Header streamed independently */}
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<main>{children}</main>
{/* Footer can be static */}
<Footer />
</body>
</html>
)
}// Data prefetching for links
import { preload } from 'react-dom'
export function prefetchProduct(productId: string) {
// Prefetch images
preload(`/api/products/${productId}/image`, { as: 'image' })
}
// Usage in a component
// <Link href={`/products/${id}`} onMouseEnter={() => prefetchProduct(id)}>/** @type {import('next').NextConfig} */
const nextConfig = {
// Build optimizations
experimental: {
// Enable Partial Prerendering (PPR)
ppr: true,
// Optimize package imports
optimizePackageImports: ['lucide-react', '@radix-ui/react-icons']
},
// Image configuration
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{ hostname: 'cdn.example.com' }
]
}
}
module.exports = nextConfigพร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
บทสรุป
React 19 Server Components เปลี่ยนแปลงวิธีการสร้างแอปพลิเคชัน React อย่างรากฐาน ประเด็นสำคัญที่ควรจดจำ:
- แยก Server/Client อย่างชัดเจน: ใช้
"use client"เฉพาะสำหรับ Component ที่ต้องโต้ตอบเท่านั้น - ดึงข้อมูลโดยตรง: ใช้
async/awaitใน Component ได้เลย ไม่จำเป็นต้องใช้ useEffect หรือ API Route - Streaming ด้วย Suspense: แสดงผลแบบค่อยเป็นค่อยไปเพื่อประสบการณ์ผู้ใช้ที่ดีขึ้น
- Server Actions: ลดความซับซ้อนของ Mutation โดยไม่ต้องสร้าง API Endpoint
- Caching อัจฉริยะ: ใช้
revalidateและtagsเพื่อเพิ่มประสิทธิภาพ - การประกอบที่ยืดหยุ่น: ใช้รูปแบบ
childrenเพื่อผสมผสาน Server และ Client Components
สถาปัตยกรรมนี้ช่วยให้สร้างแอปพลิเคชันที่มีประสิทธิภาพสูงขึ้นโดยใช้ JavaScript ฝั่ง Client น้อยลง พร้อมทั้งลดความซับซ้อนของโค้ดอย่างมาก การเปลี่ยนผ่านไปสู่ Server Components เป็นการลงทุนที่ให้ผลตอบแทนอย่างรวดเร็วทั้งในด้านประสิทธิภาพและความสามารถในการบำรุงรักษา
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
