React 19: Server Components di Produksi - Panduan Lengkap

Kuasai React 19 Server Components di lingkungan produksi. Arsitektur, pola desain, streaming, caching, dan optimasi untuk aplikasi berkinerja tinggi.

Ilustrasi React 19 Server Components yang menampilkan arsitektur server-client yang saling terhubung

Server Components merupakan evolusi paling signifikan dalam React sejak diperkenalkannya Hooks. Dengan hadirnya React 19, arsitektur ini telah matang dan siap digunakan di lingkungan produksi, memungkinkan komponen dieksekusi langsung di server sambil tetap mempertahankan interaktivitas di sisi klien.

Prasyarat

Panduan ini mengasumsikan Anda telah memahami React dan Next.js App Router. Contoh-contoh yang disajikan menggunakan Next.js 14+ yang secara bawaan telah mengimplementasikan React Server Components.

Memahami Arsitektur Server Components

Server Components (RSC) memperkenalkan paradigma baru: sebagian komponen berjalan eksklusif di server, sebagian lagi di klien, dan keduanya dapat hidup berdampingan dalam pohon komponen yang sama. Pemisahan ini secara drastis mengoptimalkan performa dengan mengurangi bundle JavaScript yang dikirim ke peramban.

Ide dasarnya berpijak pada fakta bahwa banyak komponen tidak memerlukan interaktivitas. Sebuah komponen yang menampilkan daftar artikel dari basis data, misalnya, dapat berjalan sepenuhnya di sisi server. Hanya elemen-elemen interaktif (tombol, formulir, animasi) yang memerlukan JavaScript di sisi klien.

app/articles/page.tsxtsx
// 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>
  )
}

Direktif "use client" secara eksplisit menandai komponen-komponen yang membutuhkan JavaScript di peramban.

app/articles/LikeButton.tsxtsx
'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>
  )
}

Arsitektur ini secara signifikan mengurangi bundle JavaScript: hanya kode LikeButton yang dikirim ke klien, bukan ArticlesPage maupun ArticleCard.

Pola Komposisi Server/Client

Komposisi antara Server dan Client Components mengikuti aturan yang presisi. Server Component dapat mengimpor dan merender Client Component, tetapi sebaliknya tidak dapat dilakukan secara langsung. Untuk meneruskan konten server ke client component, pola children menjadi solusinya.

components/InteractiveWrapper.tsxtsx
'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>
  )
}
app/dashboard/page.tsxtsx
// 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>
  )
}

Pola ini memungkinkan penggabungan interaktivitas klien dengan data yang di-render di server tanpa menduplikasi logika.

Pengambilan Data dan Caching

Fetch yang diperluas oleh React

React 19 secara otomatis memperluas API fetch bawaan untuk menambahkan deduplikasi dan caching. Permintaan yang identik dalam render yang sama hanya dieksekusi satu kali.

Pengambilan data di Server Components dilakukan langsung dengan async/await. React secara otomatis menangani deduplikasi permintaan yang identik.

lib/api.tstsx
// 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()
}

Untuk akses basis data secara langsung (Prisma, Drizzle), React cache dengan unstable_cache menyediakan kemampuan yang sama.

lib/db-queries.tstsx
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
}

Siap menguasai wawancara React / Next.js Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Streaming dan Suspense untuk UX Optimal

Streaming memungkinkan pengiriman HTML secara progresif ke peramban, menampilkan bagian-bagian yang sudah tersedia secara langsung sementara bagian lain masih dimuat. Dikombinasikan dengan Suspense, mekanisme ini secara drastis meningkatkan Time to First Byte (TTFB) dan pengalaman pengguna yang dirasakan.

app/product/[id]/page.tsxtsx
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>
  )
}
app/product/[id]/ProductReviews.tsxtsx
// 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>
  )
}

Peramban terlebih dahulu menerima kerangka HTML beserta skeleton, kemudian konten sesungguhnya diinjeksikan secara bertahap melalui streaming.

Server Actions untuk Mutasi

Server Actions memungkinkan eksekusi kode server dari client component tanpa perlu membuat API route. Pendekatan ini secara signifikan menyederhanakan penanganan mutasi.

actions/cart.tstsx
'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 }
}
components/AddToCartButton.tsxtsx
'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>
  )
}
Validasi di sisi server

Selalu lakukan validasi data di dalam Server Actions. Validasi di sisi klien dapat dilewati. Gunakan Zod atau pustaka serupa untuk validasi yang andal.

Penanganan Error dan Error Boundaries

React 19 meningkatkan penanganan error pada Server Components. Error Boundaries bekerja dengan cara yang sama seperti pada Client Components.

app/products/error.tsxtsx
'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>
  )
}
app/products/loading.tsxtsx
// 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>
  )
}

Untuk penanganan error yang lebih granular pada komponen tertentu, komponen ErrorBoundary dapat digunakan secara langsung.

components/ErrorBoundary.tsxtsx
'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
  }
}

Optimasi Performa untuk Produksi

Beberapa teknik memungkinkan optimasi performa Server Components di lingkungan produksi.

app/layout.tsxtsx
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>
  )
}
lib/prefetch.tstsx
// 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)}>
next.config.jstsx
/** @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

Siap menguasai wawancara React / Next.js Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Kesimpulan

React 19 Server Components secara fundamental mengubah cara aplikasi React dibangun. Poin-poin utama yang perlu diingat:

  • Server/client terpisah: gunakan "use client" hanya untuk komponen interaktif
  • Pengambilan data langsung: async/await di dalam komponen, tanpa perlu useEffect atau API route
  • Streaming dengan Suspense: tampilan progresif untuk pengalaman pengguna yang lebih baik
  • Server Actions: penyederhanaan mutasi tanpa membuat API endpoint
  • Caching cerdas: revalidate dan tags untuk optimasi performa
  • Komposisi fleksibel: pola children untuk memadukan Server dan Client Components

Arsitektur ini memungkinkan pembuatan aplikasi yang lebih berkinerja dengan JavaScript klien yang lebih sedikit, sekaligus menyederhanakan kode secara signifikan. Transisi ke Server Components merupakan investasi yang segera memberikan hasil dalam hal performa dan kemudahan pemeliharaan.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#react 19
#server components
#rsc
#performance
#next.js

Bagikan

Artikel terkait