React Server Components (RSC)はhooks以来最大のReactアーキテクチャの変革です。
React Server Componentsとは?
サーバー上でレンダリングされ、出力をクライアントに送信するコンポーネントです。
RSCモデルには3種類のコンポーネントがあります。
Server vs Client Components
| 項目 | Server Component | Client Component |
|---|---|---|
| App Routerデフォルト | はい | いいえ |
| ディレクティブ | なし | "use client" |
| クライアントJS | なし(ゼロJS) | はい |
| useState/useEffect | 利用不可 | 利用可能 |
| イベントハンドラ | 利用不可 | 利用可能 |
| DB直接アクセス | はい | いいえ |
| ファイルシステム | はい | いいえ |
| サーバー環境変数 | はい | いいえ |
| 非同期コンポーネント | はい | いいえ |
| バンドルサイズ | ゼロ | 増加 |
決定木:ServerかClientか?
Should this component be a Server or Client Component?
Does it need useState, useEffect, or useRef?
├── YES → Client Component ("use client")
└── NO ↓
Does it need event handlers (onClick, onChange, onSubmit)?
├── YES → Client Component ("use client")
└── NO ↓
Does it use browser-only APIs (window, localStorage, navigator)?
├── YES → Client Component ("use client")
└── NO ↓
Does it use third-party libraries that require hooks or browser APIs?
├── YES → Client Component ("use client")
└── NO ↓
Does it fetch data or access databases/file system?
├── YES → Server Component (default) ← BEST CHOICE
└── NO → Server Component (default) ← still the defaultRSCパターン
パターン1:サーバーサイドデータ取得
async/awaitで直接データ取得。
// app/users/page.tsx — Server Component (default, no directive needed)
import { db } from '@/lib/database';
// This component is async — only possible in Server Components
export default async function UsersPage() {
// Direct database query — this code NEVER reaches the client
const users = await db.user.findMany({
orderBy: { createdAt: 'desc' },
take: 50,
});
return (
<div>
<h1>Users ({users.length})</h1>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
</div>
);
}
// Benefits:
// - Zero JavaScript sent to client for this component
// - No loading states needed (data is available before render)
// - Database credentials never exposed to the browser
// - No useEffect + fetch + useState danceパターン2:Server-Client合成
ServerはClientをレンダリング可能。
// components/LikeButton.tsx — Client Component
'use client';
import { useState } from 'react';
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState(0);
return (
<button onClick={() => {
setLiked(!liked);
setCount(c => liked ? c - 1 : c + 1);
}}>
{liked ? 'Unlike' : 'Like'} ({count})
</button>
);
}
// app/posts/[id]/page.tsx — Server Component
import { db } from '@/lib/database';
import { LikeButton } from '@/components/LikeButton';
export default async function PostPage({ params }: { params: { id: string } }) {
// Server: fetch data
const post = await db.post.findUnique({ where: { id: params.id } });
return (
<article>
{/* Server-rendered content (zero JS) */}
<h1>{post?.title}</h1>
<div>{post?.content}</div>
{/* Client Component island (only this ships JS) */}
<LikeButton postId={params.id} />
</article>
);
}
// Pattern: Server Component as children of Client Component
// components/Sidebar.tsx
'use client';
export function Sidebar({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(true);
return (
<aside style={{ display: open ? 'block' : 'none' }}>
<button onClick={() => setOpen(!open)}>Toggle</button>
{children} {/* Server Component content passed as children */}
</aside>
);
}パターン3:Server Actions
サーバーサイド関数を直接呼び出し。
// app/actions.ts — Server Action
'use server';
import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({
data: { title, content, authorId: 'user-1' },
});
revalidatePath('/posts');
}
export async function deletePost(postId: string) {
await db.post.delete({ where: { id: postId } });
revalidatePath('/posts');
}
// components/CreatePostForm.tsx — Client Component using Server Action
'use client';
import { createPost } from '@/app/actions';
import { useActionState } from 'react';
export function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<textarea name="content" placeholder="Write your post..." required />
<button type="submit">Create Post</button>
</form>
);
}
// Works without API routes!
// The form submits directly to the server action
// Progressive enhancement: works without JavaScriptパターン4:Suspenseストリーミング
遅いデータ取得がページ全体をブロックしない。
// app/dashboard/page.tsx — Streaming with Suspense
import { Suspense } from 'react';
// Slow component — fetches from external API
async function AnalyticsChart() {
const data = await fetch('https://api.analytics.com/data', {
next: { revalidate: 60 },
});
const analytics = await data.json();
return <div>Chart: {analytics.visitors} visitors</div>;
}
// Fast component — fetches from local DB
async function RecentPosts() {
const posts = await db.post.findMany({ take: 5 });
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* This renders immediately */}
<Suspense fallback={<div>Loading posts...</div>}>
<RecentPosts />
</Suspense>
{/* This streams in when ready (doesn't block the page) */}
<Suspense fallback={<div>Loading analytics...</div>}>
<AnalyticsChart />
</Suspense>
</div>
);
}
// Result: User sees "Dashboard" + posts immediately
// Analytics chart streams in when the API responds
// No client-side loading spinners neededデータ取得戦略
逐次データ取得
依存関係がある場合。
// Sequential: second request depends on first
async function UserProfile({ userId }: { userId: string }) {
const user = await getUser(userId); // 1st request
const posts = await getUserPosts(user.name); // 2nd (depends on user.name)
return <div>{user.name}: {posts.length} posts</div>;
}並列データ取得
独立した場合はPromise.all。
// Parallel: independent requests — use Promise.all
async function Dashboard() {
// Start all requests simultaneously
const [users, posts, analytics] = await Promise.all([
getUsers(),
getPosts(),
getAnalytics(),
]);
return (
<div>
<UserList users={users} />
<PostList posts={posts} />
<AnalyticsPanel data={analytics} />
</div>
);
}キャッシュと再検証
Next.jsはfetchにキャッシュ制御を拡張。
// Next.js fetch caching strategies
// 1. Static (cached indefinitely until revalidated)
const data = await fetch('https://api.example.com/data');
// Equivalent to: { cache: 'force-cache' }
// 2. Revalidate every 60 seconds (ISR)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 },
});
// 3. Dynamic (no caching — fresh on every request)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store',
});
// 4. Tag-based revalidation
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
// In a Server Action:
import { revalidateTag } from 'next/cache';
revalidateTag('posts'); // Purge all fetches tagged 'posts'パフォーマンスの利点
RSCは従来のクライアントサイドReactに比べて複数のパフォーマンス優位性を提供。
RSC Performance Benefits:
1. Zero Client JavaScript for Server Components
- Only Client Components ship JS to the browser
- Large libraries used on server (markdown, syntax highlighting) add zero bundle
2. Streaming SSR
- HTML streams to browser as components resolve
- First Contentful Paint (FCP) is faster
- Time to Interactive (TTI) is faster (less JS to hydrate)
3. Automatic Code Splitting
- Each Client Component is automatically code-split
- No manual dynamic imports needed
4. Reduced Data Transfer
- Server Components fetch data on the server (same network)
- No client-side fetch waterfalls
- No duplicate data in both HTML and JS payload
5. Smaller Hydration Payload
- Only Client Components need hydration
- Server Component output is static — no hydration needed
Typical Results:
Before RSC: 450KB JS bundle, 2.5s TTI
After RSC: 180KB JS bundle, 1.1s TTI (60% JS reduction)よくある間違い
// Mistake 1: Adding "use client" unnecessarily
// BAD: This doesn't need client-side JavaScript
'use client'; // REMOVE THIS
export function UserCard({ name, email }: { name: string; email: string }) {
return <div>{name} ({email})</div>; // No hooks, no events = Server Component
}
// Mistake 2: Importing Server Component in Client Component
// BAD: This won't work
'use client';
import { ServerComponent } from './ServerComponent'; // ERROR
// FIX: Pass as children instead
export function ClientWrapper({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
// Mistake 3: Using "use client" too high in the tree
// BAD: Making a layout a Client Component
'use client'; // This makes EVERYTHING below it client-side
export function Layout({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('dark');
return <div className={theme}>{children}</div>;
}
// FIX: Extract the interactive part into a small Client Component
// components/ThemeToggle.tsx
'use client';
export function ThemeToggle() {
const [theme, setTheme] = useState('dark');
return <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>{theme}</button>;
}
// layout.tsx — stays as Server Component
export function Layout({ children }: { children: React.ReactNode }) {
return <div><ThemeToggle />{children}</div>;
}よくある質問
Next.jsなしでServer Componentsは使える?
技術的にはい。実用的にはNext.jsが主要なフレームワーク。
Server ComponentsはSSRの代替?
いいえ、補完します。
"use client"はいつ使う?
hooks、イベントハンドラ、ブラウザAPIが必要な時。
Server ComponentsでContextは使える?
使えません。
SEOへの影響は?
ポジティブ。サーバーでHTMLを生成。
関連ツールとガイド
- JSON Formatter - Format API responses and data
- JSON to TypeScript - Generate types from API responses
- HTML to JSX - Convert HTML to React JSX
- Next.js App Router Guide
- React Hooks Guide
- TypeScript Generics Guide
- Web Performance Optimization