Skip to content

React Server Components:フロントエンドとバックエンドの境界を再定義

React Server Components(RSC)は、単なるもう一つの SSR ソリューションではありません。React チームによる「コンポーネントはどこで実行されるべきか」という問いに対する全く新しい答えです。

SSR vs RSC

従来の SSR:サーバーサイドで HTML をレンダリングし、クライアントで完全な React コンポーネントツリーにハイドレートします。すべてのコンポーネントがクライアントバンドルに含まれます。

RSC:コンポーネントはサーバーサイドでのみ実行され、クライアントバンドルには含まれません。クライアントはレンダリング結果(特殊な JSON 形式)のみを受け取ります。

传统 SSR:
  Server: 渲染 HTML -> Client: 下载 JS -> Client: Hydrate 所有组件

RSC:
  Server: RSC 组件执行 -> 输出 RSC Payload -> Client: 只 hydrate Client 组件

Server Component vs Client Component

typescript
// デフォルトは Server Component
// app/posts/page.tsx(Next.js 14)

async function PostList() {
  // データベースに直接アクセスし、API は不要
  const posts = await db.post.findMany({
    orderBy: { createdAt: "desc" },
    take: 20,
  });

  return (
    <div>
      {posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.summary}</p>
          {/* Server Component の中に Client Component をネストできる */}
          <LikeButton postId={post.id} initialCount={post.likes} />
        </article>
      ))}
    </div>
  );
}

export default PostList;
typescript
// インタラクションが必要なコンポーネントは 'use client' でマーク
"use client";

import { useState } from "react";

interface LikeButtonProps {
  postId: string;
  initialCount: number;
}

export function LikeButton({ postId, initialCount }: LikeButtonProps) {
  const [count, setCount] = useState(initialCount);
  const [liked, setLiked] = useState(false);

  const handleLike = async () => {
    setLiked(true);
    setCount((c) => c + 1);
    await fetch(`/api/posts/${postId}/like`, { method: "POST" });
  };

  return (
    <button onClick={handleLike} disabled={liked}>
      {liked ? "已点赞" : "点赞"} ({count})
    </button>
  );
}

基本原則:デフォルトは Server Component で、useState/useEffect/イベント処理が必要な場合のみ 'use client' を追加します

データフェッチパターンの変革

typescript
// 従来のパターン:クライアントサイドで取得
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser);
  }, [userId]);
  // loading state、error state、waterfall...
}

// RSC パターン:サーバーサイドで直接取得
async function UserProfile({ userId }: { userId: string }) {
  const user = await db.user.findUnique({ where: { id: userId } });

  if (!user) notFound();

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
      <Suspense fallback={<FollowersSkeleton />}>
        <Followers userId={userId} />
      </Suspense>
    </div>
  );
}

useEffect は不要、loading 状態の管理は不要、ウォーターフォールも発生しません。データはサーバーサイドで準備され、直接レンダリングされます。

実際のアーキテクチャの考え方

Server Component を使用するケース:

  • 純粋な表示のみで、インタラクションが不要なコンポーネント
  • データ取得ロジック
  • バックエンドリソースへのアクセス(データベース、ファイルシステム、内部サービス)
  • 機密操作(トークンや秘密鍵をクライアントに公開しない)

Client Component を使用するケース:

  • useStateuseEffect を使用する
  • イベント処理(onClick、onChange)がある
  • ブラウザ API を使用する
  • context を必要とするコンポーネント

境界原則: Client Component は可能な限り小さく、できるだけリーフノードに配置します。コンポーネントツリーの大部分は Server Component であるべきです。

まとめ

  • RSC は SSR の代替ではなく、全く新しいコンポーネントモデルです
  • デフォルトは Server Component で、インタラクションが必要な場合のみ 'use client' をマークします
  • 従来のデータ取得における loading/waterfall の問題を解消します
  • サーバーサイドのロジックがクライアントに公開されず、セキュリティとバンドルサイズの両方でメリットがあります
  • メンタルモデルの変革が必要です:「すべてのコンポーネントがクライアント」から「必要に応じて実行環境を選択」へ

MIT Licensed