本記事はアジアクエスト Advent Calendar 2025の19日目の記事です。
はじめに
現在のReact、あるいはNext.jsを使う上で、第一の選択肢となるApp Router。
App Routerで実装するなら、React Server Components(以下、RSC)の理解は避けては通れません。
最近(2025年12月時点)ではRSCの脆弱性の発見により話題となり、その名を耳にした人は多いと思います。
そこで、改めてどういったものなのかをまとめてみました。
本記事では脆弱性については触れません。
あくまでRSCとは何かを整理する目的で執筆しております
0. React と Next.jsについて
本題に入る前に、現代のフロントエンド開発を支える2つの巨塔、React と Next.js について簡単におさらいしておきましょう。
React とは?
Meta(旧Facebook)が開発した、UI(ユーザーインターフェース)を構築するためのJavaScriptライブラリです。
最大の特徴は「コンポーネント指向」であり、ボタンやヘッダー、記事カードといった部品(コンポーネント)を作り、それらをブロックのように組み合わせて画面を構築します。
Next.js とは?
Vercelが開発している、Reactのためのフレームワークです。
Reactはあくまで「UIを作るためのライブラリ」であり、ルーティング(ページ遷移)やサーバー機能などは持っていません。
それら開発に必要な機能をオールインワンで提供するのがNext.jsです。
今回紹介するReact Server Components(RSC)は、名前の通りReact本体の機能です。
2020年に初めて概念として登場しました。当時はまだReactの実験的な機能でした。
2022年にリリースされたNext.js 13で実用化され、2023年のNext.js 13.4でRSCを基盤としたApp Routerを導入することで扱いやすくなりました。
React本体には、2024年にリリースされたReact 19で、安定版として導入されています。
本記事では簡単のため、主にNext.jsでのRSCについて記載していきます。
1. Pages RouterからApp Routerへ
RSCを理解するには、Pages RouterからApp Routerについても知る必要があります。
Pages Router (pages/) の世界
Next.js 13.4より前のNext.jsでは、Pages Routerという設計が用いられており、「ページ単位」でレンダリング戦略を決めていました。
- このページは SSR (
getServerSideProps) - このページは SSG (
getStaticProps)
これは分かりやすい反面、「ページ内の9割は静的なのに、ヘッダーのユーザー名表示のためだけにページ全体をSSRにする」といった非効率が発生していました。また、データ取得ロジックがページ最上部に集中し、プロップスのバケツリレーが避けられませんでした。
App Router (app/) の世界
App Router は、「コンポーネント単位」 でサーバーかクライアントかを選択できるアーキテクチャです。
- 重い処理やデータ取得はサーバーコンポーネントで
- インタラクションが必要なボタンだけクライアントコンポーネントで
ページ全体をひとまとめにするのではなく、パズルのピースごとに最適な場所でレンダリングする。これがRSCの基本思想です。
2. Server Components と Client Components の役割分担
App Routerでは、すべてのコンポーネントはデフォルトでServer Componentです。
| 特徴 | Server Components (デフォルト) | Client Components |
|---|---|---|
| 主な役割 | データ取得、バックエンド連携、静的UI | ユーザー操作、ブラウザAPI、State管理 |
| 実行場所 | サーバーのみ | クライアント(+ビルド/SSR時にサーバー) |
| バンドル | クライアントへJSが送信されない | JSとして送信される |
| 宣言 | なし | ファイル先頭に 'use client'
|
ここでの最大のメリットは 「Zero Bundle Size」 です。
例えば、サーバーコンポーネント内で日付整形ライブラリを使っても、そのライブラリ自体はブラウザに送信されません。送られるのは計算済みの結果だけです。
3. 「混ぜて使う」ためのルールと関係性
RSCやAppRouterを使う上で最も躓きやすいのが、この2つのコンポーネントの 「関係性(境界線)」 です。
インポートの「一方通行」ルール
原則として、サーバーからクライアントは呼べますが、逆はできません。
-
Server → Client: OK
サーバー側でデータを取得し、それを Client Component の Props として渡す -
Client → Server: NG
ブラウザで動くコードの中に、DB接続などのサーバーコードを含めることはできないため、直接importするとエラーになります
「穴あけパターン」 (Composition)
「じゃあ、Client Component の内側(子要素)に Server Component を表示したい時はどうするの?」
例えば、Context Provider(Client)の中に、サーバーで取得した記事リスト(Server)を表示したい場合などです。ここで登場するのが Children Props を使った Composition(合成) です。
NGパターン(直接インポート):
'use client';
import ServerList from './ServerList'; // ❌ これはできない!
export default function ClientWrapper() {
return (
<div>
<ServerList />
</div>
);
}
OKパターン(Childrenとして渡す):
親(Server Component)が責任を持って組み立て、Client Component は「穴(children)」を開けて待っている状態にします。
// ClientWrapper.tsx ('use client')
'use client';
export default function ClientWrapper({ children }: { children: React.ReactNode }) {
// ここでState管理などができる
return <div className="interactive-box">{children}</div>;
}
// page.tsx (Server Component)
import ClientWrapper from './ClientWrapper';
import ServerList from './ServerList';
export default function Page() {
return (
// 親であるServer Componentが、Clientの中にServerを「差し込む」
<ClientWrapper>
<ServerList />
</ClientWrapper>
);
}
コンポーネントツリーの骨格は Server Componentで作り、インタラクションが必要な『葉』や『中間層』だけをClient Componentにするといった考え方で理解しましょう。
4. App Router での実装例
ブログ記事の詳細ページを表示する例です。
app/blog/[slug]/page.tsx (Server Component)
import { db } from '@/lib/db';
import LikeButton from './LikeButton'; // Client Component
// async/await がコンポーネントで使える!
export default async function BlogPost({ params }: { params: { slug: string } }) {
// 1. サーバー上で直接データ取得
const post = await db.post.findUnique({ where: { slug: params.slug } });
if (!post) return <div>Not Found</div>;
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* 2. インタラクティブな部分はClient Componentに委譲 */}
<div className="mt-4">
{/* シリアライズ可能なデータ(IDなど)をPropsで渡す */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</div>
</article>
);
}
app/blog/[slug]/LikeButton.tsx (Client Component)
'use client'; // Client Componentであると宣言
import { useState } from 'react';
import { updateLikes } from '@/app/actions'; // Server Actions
export default function LikeButton({ postId, initialLikes }: { postId: string, initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
return (
<button
onClick={async () => {
const newCount = await updateLikes(postId); // サーバー処理を呼び出し
setLikes(newCount);
}}
>
👍 {likes}
</button>
);
}
このように、データ取得ロジックとUI表示(Server)、そしてインタラクション(Client)が、ファイルシステム上で自然に共存できるのが App Routerの強みです。
まとめ
RSCとApp Routerは、Reactアプリケーションの設計思想そのものを変えるアップデートでした。
- 重い処理はサーバーへ
- インタラクションはクライアントへ
というように役割分担が明確化されました。
- 基本はRSCで書く
- 必要な時だけクライアントへ降りる (
'use client') - ネストが必要なときは
childrenで合成する
この3点を意識して使いこなすことが、RSCでの開発の鍵となるでしょう。
少しでも理解の一助になれば幸いです。
参考