# 本番環境のReact Server Components:パターンと落とし穴 > 本番環境のReact Server Components:実戦で鍛えられたパターン、よくあるアンチパターン、堅牢なNext.js 15アプリケーションのためのデバッグ戦略です。 - Published: 2026-03-29 - Updated: 2026-05-04 - Author: Anthony Fillion-Maillet - Tags: react server components, next.js 15, rsc patterns, production, react 19 - Reading time: 9 min --- React Server Components(RSC)はNext.js 15におけるサーバーレンダリングの仕組みを根本から変えますが、本番投入では公式ドキュメントが必ずしもカバーしていない落とし穴が見えてきます。本記事では、機能するパターン、壊れるパターン、そして本番に達する前に問題を診断する方法を解説します。 > **Server ComponentsとClient Components** > > Server Componentはサーバー上だけで実行され、ブラウザに送られるJavaScriptはゼロです。Client Component(`"use client"`で印付け)は両側で動作します。原則として、Client Componentsはできる限り小さく、ツリーのできる限り低い位置に配置します。 ## サーバー・クライアント境界:boundaryパターンを理解する もっとも頻出するRSCの落とし穴は、ServerとClient Componentsの境界に関するものです。コンポーネントが`"use client"`ディレクティブを持った瞬間、**インポートされた子も全てClient Componentsになります**。たとえディレクティブを持たない子でも例外ではありません。 ```tsx // ProductPage.tsx (Server Component) import { ProductDetails } from './ProductDetails' import { AddToCartButton } from './AddToCartButton' export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params const product = await getProduct(id) return (
{/* Server Component: 直接DBにアクセス */} {/* Client Component: 隔離された対話性 */}
) } ``` ```tsx // AddToCartButton.tsx (Client Component) 'use client' import { useState } from 'react' export function AddToCartButton({ productId, price }: { productId: string; price: number }) { const [adding, setAdding] = useState(false) async function handleAdd() { setAdding(true) await fetch('/api/cart', { method: 'POST', body: JSON.stringify({ productId, quantity: 1 }), }) setAdding(false) } return ( ) } ``` 鍵となるパターン:Server ComponentからClient Componentへ**シリアライズ可能なprops**としてデータを渡すことです。関数、クラス、Dateオブジェクトはこの境界を越えられません。 ## アンチパターン:不要なClient Componentラッパー ありがちな誤りは、Server Componentの子をラップするClient Componentを作り、サブツリー全体をクライアント側に押し出してしまうことです。 ```tsx // PageWrapper.tsx — アンチパターン 'use client' import { useState } from 'react' // すべての子コンテンツがクライアント側になる export function PageWrapper({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState('light') return (
{children}
) } ``` > **slotとしてのchildren** > > 対処方法:Server Componentsを`children`として渡すこと(slotパターン)です。propsとして渡された子は、親がClient Componentであっても引き続きServer Componentsとして扱われます。上記のコードは、`children`がServer Componentである親から来ている限り正しく動作します。 ```tsx // layout.tsx (Server Component) import { PageWrapper } from './PageWrapper' import { HeavyServerContent } from './HeavyServerContent' export default function Layout() { return ( {/* クライアントラッパーがあってもServer Componentのまま */} ) } ``` このコンポジションパターンは、重いコンテンツに対するサーバーレンダリングの利点を保ちつつ、ラッパーレベルでの対話性を可能にします。 ## 非同期データの扱い:コンポーネント内fetchパターン React 19とNext.js 15はServer Components内で`async/await`を直接サポートします。このパターンは旧来の`getServerSideProps`方式に比べてデータ取得をシンプルにします。 ```tsx // UserProfile.tsx (Server Component) import { cache } from 'react' // 同一レンダー中の同一呼び出しを重複排除 const getUser = cache(async (userId: string) => { const res = await fetch(`https://api.example.com/users/${userId}`, { next: { revalidate: 3600 }, // 1時間キャッシュ }) if (!res.ok) throw new Error('User not found') return res.json() }) export default async function UserProfile({ userId }: { userId: string }) { const user = await getUser(userId) return (

{user.name}

{user.email}

登録日: {new Date(user.createdAt).toLocaleDateString('ja-JP')}

) } ``` 重要なポイントが3点あります: - Reactの`cache()`は単一のサーバーレンダー中に同一呼び出しを重複排除します - `next: { revalidate }`はNext.js側のキャッシュ時間を制御します - 非同期Server Componentでのエラーは最も近い`error.tsx`をトリガーします ## シリアライズの落とし穴:境界を越えられないもの ServerとClient Components間でやり取りされるデータはJSONシリアライズ可能でなければなりません。以下が静かなエラーやクラッシュを招く例です。 ```tsx // 落とし穴: シリアライズ不能な型を渡す // 関数 — 動作しない { /* server action */ }} /> // 代わりにimportしたServer Actionを使用 import { submitForm } from '@/lib/actions/form' // Dateオブジェクト — 動作しない // ISO文字列 — 動作する // Map, Set, RegExp — 動作しない // プレーンオブジェクトまたは配列 — 動作する ``` Server Actions(`"use server"`で印付けされた関数)は例外で、Next.jsがHTTPエンドポイントに変換するため、Client Componentにpropsとして渡せます。 ## ストリーミングとSuspense:段階的読み込みパターン Suspenseを用いたSSRストリーミングは、HTMLをブラウザに段階的に送ります。最適なパターンは、各非同期セクションの周りに細かなSuspense境界を設けることです。 ```tsx // DashboardPage.tsx (Server Component) import { Suspense } from 'react' import { RevenueChart } from './RevenueChart' import { RecentOrders } from './RecentOrders' import { UserStats } from './UserStats' export default function DashboardPage() { return (
}> }> }>
) } ``` 各セクションは独立して読み込まれます。`RevenueChart`が3秒、`UserStats`が200msかかる場合でも、統計はチャートを待たずに即座に表示されます。 > **SuspenseとSEO** > > Suspense境界の中身はサーバーでレンダリングされ、初期HTMLに含まれます。クローラは完全なコンテンツを認識します。ストリーミングはブラウザへの配信速度に影響するだけで、SEO上の可視性には影響しません。 ## 本番環境のデバッグ:RSCの問題を追跡する RSCのエラーはしばしば難解です。本番でも有効な3つの診断手法があります。 **1. ハイドレーションのミスマッチを特定する** ```tsx // debug-hydration.tsx 'use client' import { useEffect, useState } from 'react' export function HydrationDebug() { const [isClient, setIsClient] = useState(false) useEffect(() => { setIsClient(true) }, []) if (process.env.NODE_ENV !== 'development') return null return (
{isClient ? 'Client' : 'Server'}
) } ``` **2. RSCペイロードをログに記録する** Next.js 15では`next.config.ts`でRSCログを有効化します: ```typescript // next.config.ts const nextConfig = { logging: { fetches: { fullUrl: true, // fetchの完全なURLを表示 }, }, } export default nextConfig ``` **3. ペイロードサイズを確認する** 肥大化したRSCペイロード(> 128 KB)はパフォーマンスを低下させます。DevToolsで`text/x-component`コンテンツタイプのネットワークリクエストを監視することが望まれます。 ## 高度なパターン:Server Actionsとのコンポジション Server ActionsとServer Componentsを組み合わせると自然なCQRSパターンになります:サーバー側で読み(RSC)、actionsで書き込みです。 ```tsx // TodoList.tsx (Server Component) import { getTodos } from '@/lib/services/todo' import { TodoForm } from './TodoForm' import { deleteTodo } from '@/lib/actions/todo' export default async function TodoList() { const todos = await getTodos() return (
    {todos.map(todo => (
  • {todo.title}
  • ))}
) } ``` ```tsx // actions/todo.ts 'use server' import { revalidatePath } from 'next/cache' import { TodoService } from '@/lib/services/todo' export async function deleteTodo(formData: FormData) { const id = formData.get('id') as string await TodoService.delete(id) revalidatePath('/todos') } ``` `revalidatePath`の呼び出しにより、ページ全体を再読み込みすることなく更新済みデータでServer Componentが再レンダリングされます。 これらのトピックでより深く面接対策を行うには、SharpSkillの[Next.js Server Actions](/technologies/react-next/interview-questions/nextjs-server-actions)モジュールおよび[Next.js Data Fetching](/technologies/react-next/interview-questions/nextjs-data-fetching)モジュールを参照することをお勧めします。[React公式ドキュメント](https://react.dev/reference/rsc/server-components)はServer Componentsの完全な仕様を扱っています。 ## 結論 - Client Componentsはコンポーネントツリーの低い位置に小さく隔離して配置する - slotパターン(`children`)を用いてクライアントラッパー内のServer Componentsを保つ - サーバー・クライアント境界を越えるpropsのシリアライズ可能性を常に検証する - 独立した非同期セクションごとに細かなSuspense境界を配置する - 本番でRSCペイロードサイズを監視する(目標 < 128 KB) - 自然なCQRSパターンとしてServer Components(読み)とServer Actions(書き)を組み合わせる - Reactの`cache()`を用いて単一のサーバーレンダー内のリクエストを重複排除する --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/react-next/react-server-components-patterns-pitfalls