React Server Componentsとは ― モバイル開発における新しい選択肢
React Server Components(RSC)は、コンポーネントのレンダリングをサーバー側で実行し、その結果だけをクライアントに送る仕組みです。React 19で正式に安定版となり、Webアプリでは Next.js App Router を通じて広く採用されています。
従来、React Nativeアプリのロジックはすべてデバイス上で動作していました。データ取得にはuseEffectやReact Queryなどを用い、APIレスポンスを受け取ってから画面を構築する流れが一般的です。RSCを導入すると、データ取得とUIの組み立てをサーバーで一括処理し、最終的なUIツリーだけをネイティブアプリに配信できます。
この仕組みがReact Nativeに持ち込まれたのは、Expo SDK 52からです。Expo Routerと統合されたRSCは「Universal React Server Components」と呼ばれ、Web・iOS・Androidのすべてを単一のサーバーコンポーネントで駆動できる点が最大の特徴です。
サーバーコンポーネントとクライアントコンポーネントの境界
RSCを理解するうえで最も重要な概念は、コンポーネントが動作する「場所」の明確な区別です。
サーバーコンポーネントの特性
サーバーコンポーネントは、サーバー上(またはビルド時)にのみ実行されるReactコンポーネントです。特別なディレクティブは不要で、RSCモードではデフォルトの動作となります。
// app/index.tsx(サーバーコンポーネント — ディレクティブ不要)
import { Text, View } from 'react-native';
export default async function HomeScreen() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return (
<View>
{posts.map((post: { id: number; title: string }) => (
<Text key={post.id}>{post.title}</Text>
))}
</View>
);
}
ポイントは、コンポーネント関数にasyncキーワードを付けて直接awaitできることです。useEffectやuseStateを使った非同期処理のボイラープレートが不要になり、通常のTypeScript関数と同じ感覚でデータ取得を記述できます。
サーバーコンポーネントの制約として、以下のAPI群は使用できません。
useState、useReducer(状態管理フック)useEffect、useLayoutEffect(副作用フック)- ブラウザ専用API(
window、document) - イベントハンドラ(
onPress、onChangeなど)
クライアントコンポーネントの役割
ユーザー操作やアニメーションなどインタラクティブな要素を含む場合は、ファイル先頭に'use client'ディレクティブを記述してクライアントコンポーネントとして宣言します。
// components/LikeButton.tsx
'use client';
import { useState } from 'react';
import { Pressable, Text } from 'react-native';
export function LikeButton({ postId }: { postId: number }) {
const [liked, setLiked] = useState(false);
return (
<Pressable onPress={() => setLiked(!liked)}>
<Text>{liked ? '❤️' : '🤍'}</Text>
</Pressable>
);
}
クライアントコンポーネントはデバイス上で実行されるため、状態管理やイベント処理が可能です。重要なルールとして、サーバーコンポーネントからクライアントコンポーネントにはシリアライズ可能なデータ(文字列、数値、JSONなど)しか渡せません。関数やクラスインスタンスをpropsとして直接渡すことはできません。
使い分けの指針
| 用途 | 推奨コンポーネント | 理由 |
|---|---|---|
| API・DBからのデータ取得 | サーバー | awaitで直接取得でき、クライアントにAPIキーを露出しない |
| 静的なテキスト・画像表示 | サーバー | JavaScriptバンドルに含まれず、配信サイズが小さくなる |
| ボタンタップ・フォーム入力 | クライアント | onPressやuseStateが必要 |
| アニメーション | クライアント | Animated APIやReanimatedはデバイス上で動作 |
| 地図・カメラなどネイティブView | サーバー or クライアント | 表示だけならサーバー、操作を伴うならクライアント |
RSCとSSR(サーバーサイドレンダリング)の根本的な違い
RSCとSSRはどちらも「サーバー側で処理する」技術ですが、動作原理が大きく異なります。
SSRは完成したHTMLをサーバーで生成し、ブラウザに送ります。ブラウザ側ではそのHTMLに対してJavaScriptを「ハイドレーション」(再接続)し、インタラクティブな状態にします。つまり、SSRではコンポーネントのJavaScriptコードがクライアントにも配信されます。
一方、RSCはReact独自のシリアライズ形式(Flightプロトコル)でUIツリーの「結果」だけを送ります。サーバーコンポーネントのJavaScript自体はクライアントに送られないため、バンドルサイズの削減に直結します。さらに、RSCはHTMLではなく仮想DOMの差分として送信されるため、既存のUIツリーとマージして部分的な更新が可能です。
| 観点 | SSR | RSC |
|---|---|---|
| 送信形式 | HTML文字列 | Flightプロトコル(UIツリーのシリアライズ) |
| クライアント側JS | 全コンポーネントのコードが必要 | サーバーコンポーネントのコードは不要 |
| ハイドレーション | 必須(HTML→React接続) | サーバーコンポーネント部分は不要 |
| 部分更新 | ページ単位の再描画が基本 | コンポーネント単位で差分更新可能 |
| React Native対応 | 非対応(HTMLベースのため) | 対応(Expo Router経由) |
React Nativeにとって特に重要な点は、SSRがHTML生成を前提とするためネイティブアプリには不向きである一方、RSCのFlightプロトコルはプラットフォームに依存しない構造を持つことです。Expo Routerはこの特性を活かし、Web・iOS・Androidで同一のサーバーコンポーネントを共有する「Universal RSC」を実現しています。
Expo RouterでRSCを導入する手順
RSCを利用するには、Expo SDK 52以降とReact Native New Architectureが必要です。2026年2月時点の安定版は Expo SDK 54(React Native 0.81 / React 19.1.0)です(出典: Expo Changelog)。
前提条件
- Expo SDK 52以上
- React Native New Architecture(SDK 52以降はデフォルトで有効)
- Node.js 18以上
プロジェクトのセットアップ
# Expo SDK 54のプロジェクトを作成
npx create-expo-app@latest my-rsc-app
cd my-rsc-app
app.jsonで実験的機能フラグを有効にします。
{
"expo": {
"experiments": {
"reactServerFunctions": true
}
}
}
このフラグを有効にすると、Expo Routerのapp/ディレクトリ内のコンポーネントがデフォルトでサーバーコンポーネントとして扱われます。
サーバーコンポーネントの作成
// app/index.tsx
import { View, Text, StyleSheet } from 'react-native';
interface Article {
id: number;
title: string;
summary: string;
}
export default async function FeedScreen() {
const res = await fetch('https://api.example.com/articles');
const articles: Article[] = await res.json();
return (
<View style={styles.container}>
<Text style={styles.heading}>最新記事</Text>
{articles.map((article) => (
<View key={article.id} style={styles.card}>
<Text style={styles.title}>{article.title}</Text>
<Text>{article.summary}</Text>
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 12 },
card: { padding: 12, marginBottom: 8, backgroundColor: '#f5f5f5', borderRadius: 8 },
title: { fontSize: 16, fontWeight: '600' },
});
注意: StyleSheet.createはサーバーコンポーネント内で使用できます。ただし、インラインスタイルオブジェクトをシリアライズする形でクライアントに送信されるため、スタイルの動的変更が必要な場合はクライアントコンポーネントに分離してください。
Suspenseでローディング状態を管理
サーバーコンポーネントの非同期処理が完了するまでの間、SuspenseのfallbackにローディングUIを表示できます。
// app/index.tsx
import { Suspense } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { ArticleList } from './article-list';
export default function FeedScreen() {
return (
<View style={{ flex: 1 }}>
<Suspense fallback={<ActivityIndicator size="large" />}>
<ArticleList />
</Suspense>
</View>
);
}
// app/article-list.tsx(サーバーコンポーネント)
import { Text, View } from 'react-native';
export async function ArticleList() {
const res = await fetch('https://api.example.com/articles');
const articles = await res.json();
return (
<View>
{articles.map((a: { id: number; title: string }) => (
<Text key={a.id}>{a.title}</Text>
))}
</View>
);
}
Server Functionsでクライアントからサーバー処理を呼び出す
Server Functions(旧称: Server Actions)は、クライアントコンポーネントからサーバー側の関数を直接呼び出せる仕組みです。ファイルまたは関数の先頭に'use server'ディレクティブを付与します。
// actions/bookmark.ts
'use server';
export async function toggleBookmark(articleId: number, userId: string) {
const res = await fetch(`https://api.example.com/bookmarks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ articleId, userId }),
});
return res.json();
}
// components/BookmarkButton.tsx
'use client';
import { Pressable, Text } from 'react-native';
import { toggleBookmark } from '../actions/bookmark';
export function BookmarkButton({ articleId, userId }: { articleId: number; userId: string }) {
const handlePress = async () => {
const result = await toggleBookmark(articleId, userId);
console.log(result);
};
return (
<Pressable onPress={handlePress}>
<Text>ブックマーク</Text>
</Pressable>
);
}
Server Functionsの特性として、サーバー上で実行されるためデータベースへの直接アクセスや環境変数(process.env)の参照が可能です。APIキーやシークレットをクライアントに露出させずにバックエンド処理を記述でき、従来のREST APIエンドポイントの作成を省略できるケースがあります。
OTAアップデートとの比較 ― どちらをいつ使うか
React Nativeアプリの更新手段として、RSC以外にOTA(Over-The-Air)アップデートがあります。Expo Updates(EAS Update)やCodePushがその代表です。両者は目的と仕組みが異なるため、適切に使い分ける必要があります。
| 観点 | RSC(サーバーコンポーネント) | OTAアップデート |
|---|---|---|
| 更新タイミング | リクエストごとにリアルタイム | アプリ起動時にバンドルをダウンロード |
| 更新対象 | サーバーコンポーネントのUI・データ | JavaScriptバンドル全体 |
| ネイティブコード変更 | 不可 | 不可(ネイティブ変更はストア再審査が必要) |
| オフライン動作 | サーバー接続が必須 | ダウンロード済みバンドルで動作可能 |
| ストレージ消費 | デバイス側は最小限 | 新旧バンドルを保持するためストレージを使用 |
| ストア審査 | 不要(サーバー側の変更のみ) | 不要(JSバンドルの差し替えのみ) |
| サーバー費用 | 常時稼働サーバーが必要 | CDN配信のみ(比較的低コスト) |
RSCは「表示するデータやUIレイアウトを即座に変更したい」場面に適しています。ECアプリのキャンペーンバナー切り替えや、A/Bテストでの画面パターン出し分けなどが典型例です。
OTAアップデートは「バグ修正やロジック変更をストア審査なしに配信したい」場面で有効です。クライアント側のビジネスロジックやナビゲーション構造の修正などが代表的なユースケースです。
両者を組み合わせることも可能で、RSCで動的なUI配信を行いつつ、クライアントコンポーネントのバグ修正はOTAで配信する運用形態が考えられます。
実運用で判明した利点と課題
非同期処理の簡素化
RSCの最大のメリットは、コンポーネントを同期的な感覚で記述できることです。ある実プロダクトでの報告では、RSCの導入により「体感的に8割のReact Hooksが不要になった」とされています。useEffect + useState による非同期データ取得のパターンが丸ごと不要になり、コードの見通しが大幅に改善されます。
APIアグリゲーションの効率化
サーバーコンポーネントは複数のAPIを並列に呼び出し、結果を一つのUIツリーにまとめて返すことができます。クライアントサイドでの「ウォーターフォール型リクエスト」(前のAPIの結果を待って次のAPIを呼ぶ)を回避でき、BFF(Backend for Frontend)の役割を自然に果たします。
未解決の課題
複雑なフォーム状態の管理: 入力バリデーションや条件分岐が多いフォームでは、クライアントコンポーネントとして実装する必要があり、RSCの恩恵を受けにくい領域です。
クライアント側データフェッチの指針不足: 無限スクロールやリアルタイム検索など、初回レンダリング後にクライアントからデータ取得が必要なパターンに対する公式のベストプラクティスが確立されていません。
キャッシュ制御の複雑さ: Next.jsのprefetch機能ではリンク先のRSCペイロードを先読みしますが、意図しないリクエストが発生するケースがあります。React Native/Expo Routerでも同様の課題が今後顕在化する可能性があります。
プラットフォームの制約: RSCを本番環境で利用できるフレームワークは、2026年2月時点でNext.js App Routerに限られます。Expo Routerはデベロッパープレビュー段階であり、React Router v7も不安定版です。
RSCに関するセキュリティ上の重大な注意事項
2025年12月、RSCの実装であるFlightプロトコルに複数の深刻な脆弱性が発見されました。RSCを利用するすべてのプロジェクトで対応が必要です。
CVE-2025-55182(React2Shell)― 重大度: Critical(CVSS 10.0)
Flightプロトコルのデシリアライズ処理に起因するリモートコード実行(RCE)脆弱性です。認証なしでサーバー上の任意コードを実行可能であり、発見直後から国家レベルの攻撃者による悪用が確認されています(出典: Google Cloud Threat Intelligence)。
CVE-2025-55184 / CVE-2025-67779 ― サービス拒否(CVSS 7.5)
Server Functionエンドポイントへの不正なHTTPリクエストによって、サーバーが無限ループに陥るDoS脆弱性です(出典: React公式ブログ)。
CVE-2025-55183 ― ソースコード漏洩(CVSS 5.3)
特殊なHTTPリクエストでServer Functionのソースコードが露出する脆弱性です。ハードコードされたシークレットが含まれていた場合、情報漏洩につながります。
対策
すべての脆弱性を修正したReactのバージョンは以下の通りです。
- React 19.0.4
- React 19.1.5
- React 19.2.4
Expo SDK 54ユーザーはreact-server-dom-webpackを19.1.5以上に更新してください。19.1.4ではDoS(CVE-2025-67779)とソースコード漏洩(CVE-2025-55183)の修正が不完全です(出典: Expo Security Advisory)。
影響を受けるフレームワークはNext.js、Expo Router、React Router、Waku、@parcel/rsc、@vitejs/plugin-rsc、rwsdkなど、RSCを実装するすべてのフレームワークです。
RSCに対応するフレームワークの現状
| フレームワーク | RSC対応状況 | 備考 |
|---|---|---|
| Next.js(App Router) | 安定版・本番利用可 | App RouterでServer Componentsがデフォルト |
| Expo Router | デベロッパープレビュー | モバイルネイティブ初のRSC実装。本番利用は非推奨 |
| React Router v7 | プレビュー(unstable) | unstable_reactRouterRSCプラグインで利用可能 |
| Waku | アルファ版 | RSCに特化したミニマルなReactフレームワーク |
| @vitejs/plugin-rsc | 実験的 | ViteベースのRSC基盤。React Router・Wakuと共通 |
Expo RouterのRSCが「デベロッパープレビュー」である点は強調しておく必要があります。Expo公式ドキュメントには「Expo Routerでデフォルト有効化する予定の機能の早期プレビュー」と記載されていますが、RouterとReact Navigationの並行処理対応のための書き直しが必要であり、完成時期は明示されていません(出典: Expo Documentation)。
デプロイ構成
RSCを利用するアプリは、サーバーの常時稼働が必要です。Expo Routerの場合、以下のデプロイパターンがあります。
Webアプリの場合
Express、Hono、その他のNode.jsサーバーフレームワーク上でExpo Routerのサーバーサイドを実行できます。
ネイティブアプリの場合
EAS Hosting(Expo Application Services)を利用してサーバーをデプロイします。ネイティブアプリはこのサーバーにHTTPリクエストを送り、RSCペイロードを取得する構成です。
ビルドタイム レンダリング
サーバーを使わずに、ビルド時にサーバーコンポーネントを実行して静的なコンテンツを生成する方法もあります。更新頻度の低いページでは、サーバー運用コストを抑えつつRSCの記述メリットを享受できます。
まとめ
React Server ComponentsがReact Nativeに導入されたことで、モバイルアプリ開発においてもサーバー駆動のUI構築が可能になりました。非同期データ取得の簡素化、バンドルサイズの削減、APIキーの秘匿化など、従来のクライアントサイド完結型アーキテクチャでは実現しにくかったメリットが得られます。
一方で、Expo RouterのRSCサポートはデベロッパープレビュー段階であり、本番プロダクトへの投入は慎重に判断すべきです。2025年末に発見されたCVE-2025-55182をはじめとするセキュリティ脆弱性への対応も必須であり、Reactを最新のパッチバージョン(19.0.4 / 19.1.5 / 19.2.4)に更新することが強く推奨されます。
技術選定としては、Web向けのRSCはNext.js App Routerで安定運用が可能です。React Nativeアプリへの適用は、Expo Routerの正式リリースを待ちつつ、デベロッパープレビューでプロトタイプや検証を進めるのが現実的なアプローチです。
