React 19: Server Componentsを本番環境で活用する完全ガイド
React 19 Server Componentsを本番環境で実装する方法を解説します。アーキテクチャ設計、コンポジションパターン、ストリーミング、キャッシュ戦略、パフォーマンス最適化まで網羅します。

Server Componentsは、Hooks以来のReactにおける最も大きなアーキテクチャの進化です。React 19でこの仕組みは成熟し、本番運用に耐えるレベルに到達しました。コンポーネントをサーバー上で直接実行しながら、クライアント側のインタラクティビティも維持できます。
本ガイドはReactとNext.js App Routerの基本を理解していることを前提としています。サンプルコードにはReact Server ComponentsをネイティブにサポートするNext.js 14以降を使用しています。
Server Componentsのアーキテクチャを理解する
Server Components(RSC)は新しいパラダイムを導入します。一部のコンポーネントはサーバーでのみ実行され、一部はクライアントでのみ実行され、両者は同じコンポーネントツリー内で共存できます。この分離により、ブラウザに送信されるJavaScriptバンドルが大幅に最適化されます。
根本的な考え方は、多くのコンポーネントにはインタラクティビティが不要であるという事実に基づいています。たとえば、データベースから記事一覧を表示するコンポーネントは、完全にサーバーサイドで実行できます。クライアント側のJavaScriptが必要なのは、ボタン、フォーム、アニメーションなどのインタラクティブな要素だけです。
// 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>
)
}"use client" ディレクティブは、ブラウザの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バンドルは大幅に削減されます。クライアントに送信されるのは LikeButton のコードだけで、ArticlesPage や ArticleCard のコードは含まれません。
サーバー/クライアントのコンポジションパターン
Server ComponentsとClient Componentsのコンポジションには明確なルールがあります。Server ComponentsはClient Componentsをインポートしてレンダリングできますが、逆は直接できません。サーバーのコンテンツを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>
)
}このパターンにより、ロジックの重複なしに、クライアントのインタラクティビティとサーバーレンダリングされたデータを組み合わせることができます。
データフェッチとキャッシュ
React 19はネイティブの fetch APIを自動的に拡張し、重複排除とキャッシュの機能を追加します。同じレンダリング内で同一のリクエストは一度だけ実行されます。
Server Componentsでのデータフェッチは、async/await で直接行います。Reactは同一リクエストの重複排除を自動的に処理します。
// 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、技術テストで練習しましょう。
ストリーミングとSuspenseによる最適なUX
ストリーミングを使用すると、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シェルを受信し、その後ストリーミングを通じて実際のコンテンツが段階的に挿入されます。
Server Actionsによるミューテーション
Server Actionsを使用すると、APIルートを作成することなく、Client Componentからサーバーコードを実行できます。このアプローチにより、ミューテーション処理が大幅に簡素化されます。
'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では必ずデータを検証してください。クライアントサイドのバリデーションはバイパス可能です。堅牢なバリデーションには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>
)
}特定のコンポーネントでより細かいエラーハンドリングが必要な場合は、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
}
}本番環境でのパフォーマンス最適化
本番環境でServer Componentsのパフォーマンスを最適化するためのテクニックをいくつか紹介します。
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 = nextConfigReact / Next.jsの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
まとめ
React 19 Server Componentsは、Reactアプリケーションの構築方法を根本的に変えます。重要なポイントを整理します。
- サーバーとクライアントの分離: インタラクティブなコンポーネントにのみ
"use client"を使用する - 直接的なデータフェッチ: コンポーネント内で
async/awaitを使用し、useEffectやAPIルートは不要 - Suspenseによるストリーミング: 段階的な表示で体感UXを向上させる
- Server Actions: APIエンドポイントを作成せずにミューテーションを簡素化する
- スマートキャッシュ:
revalidateとtagsでパフォーマンスを最適化する - 柔軟なコンポジション:
childrenパターンでServer ComponentsとClient Componentsを組み合わせる
このアーキテクチャにより、クライアントサイドのJavaScriptを削減しながら、より高性能なアプリケーションを構築でき、コードも大幅に簡素化されます。Server Componentsへの移行は、パフォーマンスと保守性の両面ですぐに成果が出る投資です。
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
タグ
共有
