# React 19: Server Componentsを本番環境で活用する完全ガイド > React 19 Server Componentsを本番環境で実装する方法を解説します。アーキテクチャ設計、コンポジションパターン、ストリーミング、キャッシュ戦略、パフォーマンス最適化まで網羅します。 - Published: 2026-01-08 - Updated: 2026-04-06 - Author: SharpSkill - Tags: react 19, server components, rsc, performance, next.js - Reading time: 14 min --- Server Componentsは、Hooks以来のReactにおける最も大きなアーキテクチャの進化です。React 19でこの仕組みは成熟し、本番運用に耐えるレベルに到達しました。コンポーネントをサーバー上で直接実行しながら、クライアント側のインタラクティビティも維持できます。 > **前提知識** > > 本ガイドはReactとNext.js App Routerの基本を理解していることを前提としています。サンプルコードにはReact Server ComponentsをネイティブにサポートするNext.js 14以降を使用しています。 ## Server Componentsのアーキテクチャを理解する Server Components(RSC)は新しいパラダイムを導入します。一部のコンポーネントはサーバーでのみ実行され、一部はクライアントでのみ実行され、両者は同じコンポーネントツリー内で共存できます。この分離により、ブラウザに送信されるJavaScriptバンドルが大幅に最適化されます。 根本的な考え方は、多くのコンポーネントにはインタラクティビティが不要であるという事実に基づいています。たとえば、データベースから記事一覧を表示するコンポーネントは、完全にサーバーサイドで実行できます。クライアント側のJavaScriptが必要なのは、ボタン、フォーム、アニメーションなどのインタラクティブな要素だけです。 ```tsx // app/articles/page.tsx // 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 (

Recent Articles

{articles.map((article) => ( // ArticleCard is also a Server Component {/* LikeButton is a Client Component (interactive) */} ))}
) } ``` `"use client"` ディレクティブは、ブラウザのJavaScriptを必要とするコンポーネントを明示的にマークします。 ```tsx // app/articles/LikeButton.tsx '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 ( ) } ``` このアーキテクチャにより、JavaScriptバンドルは大幅に削減されます。クライアントに送信されるのは `LikeButton` のコードだけで、`ArticlesPage` や `ArticleCard` のコードは含まれません。 ## サーバー/クライアントのコンポジションパターン Server ComponentsとClient Componentsのコンポジションには明確なルールがあります。Server ComponentsはClient Componentsをインポートしてレンダリングできますが、逆は直接できません。サーバーのコンテンツをClient Componentに渡すには、`children` パターンが有効です。 ```tsx // components/InteractiveWrapper.tsx '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
{children}
} return (
{isExpanded && (
{/* 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 */}
) } ``` このパターンにより、ロジックの重複なしに、クライアントのインタラクティビティとサーバーレンダリングされたデータを組み合わせることができます。 ## データフェッチとキャッシュ > **React拡張されたfetch** > > React 19はネイティブの `fetch` APIを自動的に拡張し、重複排除とキャッシュの機能を追加します。同じレンダリング内で同一のリクエストは一度だけ実行されます。 Server Componentsでのデータフェッチは、`async/await` で直接行います。Reactは同一リクエストの重複排除を自動的に処理します。 ```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 } ``` ## ストリーミングとSuspenseによる最適なUX ストリーミングを使用すると、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!

) } return (
{reviews.map((review) => (
{review.author} {'★'.repeat(review.rating)}{'☆'.repeat(5 - review.rating)}

{review.content}

))}
) } ``` ブラウザはまずスケルトンを含むHTMLシェルを受信し、その後ストリーミングを通じて実際のコンテンツが段階的に挿入されます。 ## Server Actionsによるミューテーション Server Actionsを使用すると、APIルートを作成することなく、Client Componentからサーバーコードを実行できます。このアプローチにより、ミューテーション処理が大幅に簡素化されます。 ```tsx // actions/cart.ts '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 } } ``` ```tsx // components/AddToCartButton.tsx '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 ( ) } ``` > **サーバーサイドバリデーション** > > Server Actionsでは必ずデータを検証してください。クライアントサイドのバリデーションはバイパス可能です。堅牢なバリデーションにはZodなどのライブラリを使用してください。 ## エラーハンドリングとError Boundaries React 19では、Server Componentsのエラーハンドリングが改善されました。Error BoundariesはClient Componentsと同じ仕組みで動作します。 ```tsx // app/products/error.tsx 'use client' // Error Boundary for /products segment interface ErrorProps { error: Error & { digest?: string } reset: () => void } export default function ProductsError({ error, reset }: ErrorProps) { return (

Loading Error

Unable to load products. Please try again.

{/* Display digest for debugging */} {error.digest && (

Reference: {error.digest}

)}
) } ``` ```tsx // app/products/loading.tsx // Loading UI during initial load export default function ProductsLoading() { return (
{[1, 2, 3, 4, 5, 6].map((i) => (
))}
) } ``` 特定のコンポーネントでより細かいエラーハンドリングが必要な場合は、`ErrorBoundary` コンポーネントを直接使用できます。 ```tsx // components/ErrorBoundary.tsx 'use client' import { Component, ReactNode } from 'react' interface Props { children: ReactNode fallback?: ReactNode } interface State { hasError: boolean error?: Error } export class ErrorBoundary extends Component { 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 || (

An error occurred

) } return this.props.children } } ``` ## 本番環境でのパフォーマンス最適化 本番環境でServer Componentsのパフォーマンスを最適化するためのテクニックをいくつか紹介します。 ```tsx // app/layout.tsx 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 ( {/* Header streamed independently */} }>
{children}
{/* Footer can be static */}