{/* children can contain Server Components */}
{children}
)}
)
}
```
```tsx
// app/dashboard/page.tsx
// 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 (
{/* Stats in an expandable wrapper */}
{/* This content is rendered server-side then passed to client */}
{/* Recent activity */}
)
}
```
รูปแบบนี้ช่วยให้สามารถผสมผสานการโต้ตอบฝั่ง Client เข้ากับข้อมูลที่ Render บนเซิร์ฟเวอร์ได้โดยไม่ต้องทำซ้ำ Logic
## การดึงข้อมูลและ Caching
> **Fetch ที่ถูกขยายโดย React**
>
> React 19 ขยาย API `fetch` ดั้งเดิมโดยอัตโนมัติเพื่อเพิ่มการขจัดความซ้ำซ้อนและ Caching Request ที่เหมือนกันภายใน Render เดียวกันจะถูกเรียกใช้เพียงครั้งเดียว
การดึงข้อมูลใน Server Components ทำได้โดยตรงด้วย `async/await` React จัดการการขจัด Request ที่ซ้ำซ้อนโดยอัตโนมัติ
```tsx
// lib/api.ts
// 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` มอบความสามารถเดียวกัน
```tsx
// lib/db-queries.ts
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
}
```
## Streaming และ Suspense เพื่อประสบการณ์ผู้ใช้ที่ดีที่สุด
Streaming ช่วยให้สามารถส่ง HTML ไปยังเบราว์เซอร์แบบค่อยเป็นค่อยไป แสดงส่วนที่พร้อมแล้วทันทีในขณะที่ส่วนอื่นยังคงโหลดอยู่ เมื่อใช้ร่วมกับ Suspense กลไกนี้ช่วยปรับปรุง Time to First Byte (TTFB) และประสบการณ์ผู้ใช้ได้อย่างเห็นได้ชัด
```tsx
// app/product/[id]/page.tsx
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 (
{/* Immediate render with product data */}
{/* Reviews load via streaming */}
Customer Reviews
}>
{/* This async component will be streamed */}
{/* Recommendations too */}
Similar Products
}>
)
}
// Skeleton for reviews
function ReviewsSkeleton() {
return (
{[1, 2, 3].map((i) => (
))}
)
}
```
```tsx
// app/product/[id]/ProductReviews.tsx
// 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 (
No reviews yet. Be the first to share your thoughts!