React

Remix SPAモードとは?ssr:falseの設定と制約、React Router移行の判断

Remix SPAモードとは?ssr:falseの設定と制約、React Router移行の判断

Remixでサーバーを持たずに静的ホスティングへ配信したいとき、選択肢になるのがSPAモードです。ただしこのモードは「サーバーレンダリングをやめるだけ」ではありません。loader と action の export はビルドエラーになり、初期HTMLにはルートルートより深い内容が入りません。さらに2024年11月以降、Remix v2の後継はReact Router v7へ移り、SPAモードの仕様もそちらで先に更新されています。ここでは公式ドキュメントとnpmの公開版で確認した仕様に絞り、設定手順・制約・移行判断を整理します。

まとめ:Remix SPAモードの要点

  • SPAモードはRemix 2.5.0で追加された機能で、ViteおよびRemix Viteプラグインの利用が前提です。
  • 有効化は vite.config.ts で ssr: false を指定するだけですが(2.5.x/2.6.x では unstable_ssr)、その瞬間に headers・loader・action は export した時点でビルドが失敗します。
  • データ取得と更新は clientLoader・clientAction に一本化されます。これらから serverLoader・serverAction は呼び出せません。
  • index.html はルートルートの HydrateFallback をビルド時に事前レンダリングして生成されます。ルート以下のページ内容は初期HTMLに含まれません。
  • 新規プロジェクトでRemix v2のSPAモードを選ぶ理由は2026年8月時点でほぼありません。後継のReact Router(最新 8.3.0)ではルートルートの loader が使えるなど制約が緩和されています。

以降で、設定・制約・デプロイ・SEO・移行判断の順に、公式ドキュメントの記述と対応させながら見ていきます。

SPAモードの位置づけと2.5.0での追加背景

Remixは本来「サーバーアーキテクチャは開発者が所有する」という前提のフレームワークです。それでも公式ドキュメントは、サーバーを運用したくない場合、GitHub PagesやCDNへ静的ファイルとして配信したい場合、サーバーレンダリングできない組み込み用途などを現実に存在する妥当なユースケースとして挙げ、そのためにSPAモードを2.5.0で追加したと説明しています。

ドキュメントはSPAモードの実体を「React Router + Vite で createBrowserRouter/RouterProvider を自前構成した場合に得られるもの」に、ファイルベースルーティング、ルート単位の自動コード分割、<Link prefetch>、<Meta>/<Links> による head 管理を足したものと位置づけています。つまりSPAモードは別系統の機能ではなく、Remixの規約をクライアント側だけで走らせる構成です。

ssr:falseによる有効化とindex.htmlの生成

vite.config.tsでの指定とテンプレートからの生成

既存のRemix+Viteアプリでは、Remix Viteプラグインの設定に ssr: false を渡します。

import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    remix({
      ssr: false,
    }),
  ],
});

ここでバージョンを確認してください。このフラグ名は途中で変わっています。SPAモードが追加された2.5.0では unstable_ssr で、v2.7.0のCHANGELOGにある「Vite: Stabilize “SPA Mode” by renaming the Remix vite plugin config from unstable_ssr -> ssr」で現在の名前に落ち着きました。2.5.x系や2.6.x系を使ったまま ssr: false と書いても効きません。

新規なら公式テンプレートを使うほうが確実です。開発サーバーは通常のRemixと同じく npx remix vite:dev で起動し、HMRもそのまま効きます。

npx create-remix@latest --template remix-run/remix/templates/spa

HydrateFallbackから生成されるindex.html

npx remix vite:build を実行すると、Remixは / のサーバーハンドラを呼び出し、レンダリング結果を既定で build/client/index.html に保存します。この index.html は root.tsx の HydrateFallback コンポーネントから生成されます。

export function HydrateFallback() {
  return (
    <>
      <p>Loading...</p>
      <Scripts />
    </>
  );
}

export default function Component() {
  return (
    <>
      <Outlet />
      <Scripts />
    </>
  );
}

ここで押さえるべきは、公式ドキュメントの「index.htmlを生成する初回レンダリングにはrootより深いルートは一切含まれない」という記述です。これは欠陥ではなく設計で、生成された1枚の index.html を /about のような任意のパスでも配信・ハイドレートできるようにするための割り切りです。<Scripts> を HydrateFallback に置き忘れると、生成されたHTMLからJavaScriptの読み込みが落ちてハイドレート自体が始まりません。

SPAモードで使えなくなるサーバーAPIの境界

ここが最も誤解の多い部分です。SPAモードでは、次の制約が公式に明記されています。

  • headers・loader・action といったサーバーAPIは使用できず、export した時点でビルドがエラーを投げます。
  • HydrateFallback を export できるのは root.tsx だけで、他のルートから export するとビルドがエラーを投げます。
  • clientLoader/clientAction から serverLoader/serverAction は呼び出せません。動かすサーバーが存在しないためです。

「SPAモードでもloaderでデータを取得する」と説明している記事や、SPAモードの章に export let loader を載せているサンプルは、この時点で成立しません。ビルドが通らないため、コピーしてもアプリは起動しません。

clientLoader・clientActionによるデータ取得と更新

データ取得と更新は Client Data API に一本化されます。書き方はサーバー版とほぼ同じで、関数名が変わります。

export async function clientLoader({ params }) {
  const res = await fetch(`/api/items/${params.id}`);
  return res.json();
}

export async function clientAction({ request }) {
  const formData = await request.formData();
  const res = await fetch("/api/items", {
    method: "POST",
    body: formData,
  });
  return res.json();
}

React Routerアプリから移行する場合、公式が示す手順も「すべての loader/action を clientLoader/clientAction にリネームする」ことです。関数の引数や戻り値の扱いはサーバー版と共通なので、両者の役割分担はReact Routerのaction・loaderとclientAction・clientLoaderの違いの整理がそのまま使えます。

設計上の注意は1点あります。APIキーを伴う外部サービス呼び出しやデータベース直結など、サーバー側でしか実行できない処理はSPAモードに置き場所がありません。別途API層を用意する前提になります。

SPAフォールバック設定とビルド時レンダリングの落とし穴

すべてのパスをindex.htmlへ向ける配信設定

生成物は静的ファイルなので任意のHTTPサーバーで配信できますが、単一の index.html を複数パスに対して返す設定(SPAフォールバック)が必要です。公式は簡易例として sirv-cli を挙げています。

npx sirv-cli build/client/ --single

ホスティングサービスによっては _redirects のようなファイルで同じ指定を行います。有効なはずのURLで404が出る場合、まず疑うのはこの設定です。Cloudflare Pagesへ載せる構成はRemixとCloudflare Pagesを使ったサイト構成のセットアップ手順で扱っています。AWS Amplify Hostingに載せる場合は書き換え規則の記法が独特なので、Remix SPAモードとAWS Amplify Gen 2の構成手順を参照してください。

ビルド時サーバーレンダリングによるwindow・document参照エラー

SPAを作っているつもりでも、index.html を生成するためにビルド時のサーバーレンダリングは走ります。Remix v2のドキュメントは「SPAを作っているとはいえ、ビルド工程としてのサーバービルドとサーバーレンダリングは依然として存在する」と明言したうえで、document・window・localStorage のようなクライアント専用APIを参照する依存関係への注意を促しています。回避策として挙げられているのは、ブラウザ専用ライブラリを entry.client.tsx からインポートしてサーバービルドに含めない方法、React.lazy の利用、remix-utils の <ClientOnly> コンポーネントの3つです。依存パッケージのCJS/ESM問題で落ちる場合は、Viteの ssr.noExternal に該当パッケージを指定してサーバービルドへバンドルします。

SPAモードのSEO上の限界とhead管理

初期HTMLがルートシェルだけになる制約

SPAモードには実行時のサーバーレンダリングが存在しません。配信されるHTMLは、ビルド時にルートルートから生成した1枚だけです。したがって「SPAモードでもSSRによってSEOを強化できる」という説明は成立しません。クローラが最初に受け取るHTMLには、記事本文も商品情報もタイトルの出し分けも含まれず、レンダリング後にしか現れません。

検索流入が事業要件に入っているなら、SPAモードは選ぶべきではありません。判断基準は単純で、URLごとに異なる内容が検索対象になるならサーバーレンダリングか事前レンダリングを使い、認証後の管理画面や社内ツールのようにインデックスさせないものだけをSPAモードに寄せる、という切り分けになります。

事前レンダリングを併用する場合の緩和

React Router側では prerender オプションを併用できます。対象パスを配列で列挙するか、非同期関数で返します。

import type { Config } from "@react-router/dev/config";

let slugs = getPostSlugs();

export default {
  ssr: false,
  prerender: ["/", "/blog", ...slugs.map((s) => `/blog/${s}`)],
} satisfies Config;

ビルドすると、パスごとに初回ドキュメント要求用の [url].html と、クライアント側遷移が読む [url].data が出力されます。このとき制約が一部緩み、事前レンダリング対象のパスにマッチするルートは loader を持てます。公式ドキュメントの注記どおり、action と headers は ssr: false のままである限り引き続き使えません。URLを有限に列挙できるコンテンツであれば、この組み合わせが現実的な落としどころです。

MetaDescriptor配列で返すv2のmeta仕様

head管理は <Meta>/<Links> APIで行いますが、Remix v2の meta はオブジェクトではなく MetaDescriptor の配列を返します。v1形式の return { title, description } はv2では機能しません。

export const meta: MetaFunction = () => {
  return [
    { title: "Very cool app | Remix" },
    { property: "og:title", content: "Very cool app" },
    { name: "description", content: "This app is the best" },
  ];
};

公式ドキュメントはこれらの記述子について「HTMLタグと1対1で対応する」と説明しています。{ title } は <title> タグを生成し、description は name と content のペアで書きます。スタイルシートやファビコンは meta ではなく links の担当です。設定の全体像はReact Routerのmeta・title設定と基本ルーティングと共通しています。

Remix v2のSPAモードを新規採用すべきでない条件

React Router v7への移行手順とcodemodの適用範囲

React Router公式の移行ドキュメントは、v7を「Remix v2の次のメジャーバージョン」と明記しています。実際、remix.run/docs/... へアクセスすると旧版扱いの v2.remix.run へリダイレクトされ、そのページ上部には React Router v7 のリリース告知が常時表示されます。Remix v2のfutureフラグをすべて有効にしてあれば、移行作業の中心は依存パッケージとimport元の入れ替えです。@remix-run/node などランタイム固有パッケージに再エクスポートされていた共通APIは react-router へ集約されました。

npx codemod remix/2/react-router/upgrade

このcodemodは依存の更新、import元の書き換え、package.json のスクリプト変更(remix vite:dev → react-router dev、remix vite:build → react-router build)、エントリファイルの RemixServer → ServerRouter/RemixBrowser → HydratedRouter のリネームまで自動化します。

要件は移行先の系列で変わります。v7は node@20 / react@18 / react-dom@18 が下限ですが、最新のv8は [email protected] 以上、[email protected]/[email protected] 以上を要求し、フレームワークモードではさらに vite@7 以上(future.v8_viteEnvironmentApi の有効化を伴います)が必要です。2026年8月3日時点のnpm公開版は @remix-run/react が 2.17.5、react-router が 8.3.0(2026年7月22日公開、v8.0.0は同年6月17日)で、v7系も 7.18.2(2026年7月28日公開)として保守されています。React 19系へ上げられないプロジェクトは、まずv7系へ寄せるのが現実的です。移行のタイミングでルーター自体を見直すなら、React/SPA向けの型安全ルーターTanStack Routerも比較対象に入ります。

なおRemixという名前は、v2の延長ではない新しいフレームワーク「Remix 3」を指すようになりました。npm の remix パッケージは 3.0.0-beta.5(dist-tag は next、2026年7月1日)が最新で、latest は 2.17.5 のままです。設計思想はWeb標準ベースで組み直されたRemix 3のコンポーネントライブラリで扱っています。

React Router 7.2.0以降のルートloader対応

移行によってSPAモードの制約そのものが緩みます。設定先は react-router.config.ts に変わります。

import { type Config } from "@react-router/dev/config";

export default {
  ssr: false,
} satisfies Config;

React Router 7.2.0(2025年2月18日公開)のCHANGELOGにある「Allow a root loader in SPA Mode」により、ルートルートに限り loader を書けるようになりました。公式ドキュメントは ssr:false の効果を「実行時のサーバーレンダリングだけを無効化する。ルートルートはビルド時にサーバーレンダリングされて index.html を生成する」と説明しており、ルートの loader はそのビルド時に実行され、結果を HydrateFallback の loaderData プロパティから受け取れます。Remix v2では loader の export そのものがビルドエラーだったので、挙動の違いを理解せずに移行するとここで戸惑います。

v8へ上げる場合は meta 周りの破壊的変更も併せて確認してください。meta と matches の data が廃止されて loaderData に統一され、react-router-dom パッケージ自体がなくなってDOM固有APIは react-router/dom からのimportになります。Cloudflare環境では cloudflareDevProxy を @cloudflare/vite-plugin に置き換えます。

SPAモードに関係する既知脆弱性と修正バージョン

SPAモードと事前レンダリングの周辺では、修正済みの脆弱性が複数公表されています。バージョンを固定したまま運用している場合は確認が要ります。

識別子 内容 react-router 修正版 @remix-run/react 修正版
CVE-2025-43864 SPAモード強制のキャッシュポイズニングでDoS 7.2.0以上 7.5.1以下 7.5.2 対象外 –
CVE-2025-43865 事前レンダリングデータのなりすまし 7.0.0-pre.0以上 7.5.1以下 7.5.2 対象外 –
CVE-2025-59057 script:ld+json生成時のXSS 7.0.0以上 7.8.2以下 7.9.0 1.15.0以上 2.17.0以下 2.17.1
CVE-2026-21884 ScrollRestoration経由のXSS 7.0.0以上 7.12.0未満 7.12.0 2.17.3未満 2.17.3

いずれもGitHub Security Advisoryでの深刻度はHighです。前2件は2025年4月24日、後2件は2026年1月8日の公表で、後2件はサーバーレンダリング時に発火します。CVE-2026-21884のアドバイザリは「フレームワークモードでサーバーレンダリングを無効化しているアプリケーションには影響しない」と明記しており、ssr: false の構成はこの点で有利です。裏を返せば、SSRへ戻す判断をした時点でこれらは自分の問題になります。

ただし上表は、SPAモードと事前レンダリングに直接関わるものだけを抜き出したものです。react-routerには2026年6月以降もDoSやRCE相当のアドバイザリが継続して公表されており(turbo-stream経由の任意コンストラクタ呼び出しは7.14.2、経路マッチングのDoSは7.18.0で修正)、上表の修正版に上げれば十分という意味ではありません。v7系なら7.18.2、新規なら8.3.0というように、系列内の最新パッチへ追随してください。なお8.3.0で修正されたRSCモードのCSRF(GHSA-qwww-vcr4-c8h2、2026年7月24日公表)はv7系に修正版がありませんが、アドバイザリが対象を不安定版のRSC APIに限定しているため、それを使っていなければv7系に留まる判断は成立します。

よくある質問

Remix SPAモードとReact RouterのSPAモードは何が違いますか?

設定ファイルが vite.config.ts から react-router.config.ts へ変わり、React Router 7.2.0以降はルートルートに loader を書けます。Remix v2では loader のexportはビルドエラーでした。加えてReact Router側では prerender を併用でき、事前レンダリング対象パスのルートは loader を持てます。

SPAモードでloaderを使うとどうなりますか?

ビルドがエラーで停止します。公式ドキュメントは headers・loader・action をexportするとビルドがエラーを投げると明記しています。データ取得は clientLoader、更新は clientAction を使ってください。これらから serverLoader・serverAction を呼ぶこともできません。

SPAモードでもSEO対策はできますか?

初期HTMLはルートルートから生成した1枚だけなので、URLごとの内容は初期HTMLに含まれません。検索流入が必要なページは、サーバーレンダリングに戻すか、React Routerの prerender で対象パスを静的HTML化するのが現実的です。事前レンダリングではパスごとに [url].html と [url].data が生成されます。

既存のRemix v2 SPAアプリはReact Routerへ移行すべきですか?

futureフラグを有効にしてあれば、移行の中心は依存とimportの入れ替えで、npx codemod remix/2/react-router/upgrade が用意されています。判断の分かれ目は要件で、v7は node 20 / React 18 以上、v8は node 22.22 / React 19.2.7 以上が必要です。React 19系へ上げられないうちは、v7系の最新パッチに留めるのが安全です。

ビルド結果はどこに出力されますか?

既定で build/client/ です。index.html もこの直下に生成されます。React Router側でも同じで、react-router build の実行後は build/client ディレクトリを静的ホストへ配置します。ssr: false ではデプロイ対象のサーバーバンドルが出力されないため、Nodeプロセスを常駐させる必要はありません。ビルド工程としてのサーバーレンダリングが走る点とは別の話です。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.15 テックブログ リゾートトラスト不正アクセス:会員2.6万件・カハラ横浜3.6万件の対象確認と窓口
  2. 2025.09.03 コラム ストループ効果とは?原典1935年の実測値と年齢別の平均・テストのやり方
  3. 2026.10.16 テックブログ バーガーキング不正アクセスで約1,118万件の情報漏洩|対象確認・案内メールの真偽と運営側のログ点検
  4. 2025.09.03 コラム Google AIモードの履歴の消し方と残る理由|いつまで残るか・使い方・オフにする方法【2026年10月】
  5. 2026.01.20 テックブログ PineDramaとは?TikTokのショートドラマアプリの日本配信・料金・安全性を解説

RELATED POSTS 関連記事

目次