React v19 + React Router v7 の SPA モードを CloudFront + S3 に AWS CDK でデプロイしてみた

React v19 + React Router v7 の SPA モードを CloudFront + S3 に AWS CDK でデプロイしてみた

Clock Icon2025.05.29

こんにちは、製造ビジネステクノロジー部の若槻です。

前回のブログで React v19 + React Router v7 のアプリケーションを create-react-app で作成してみました。

https://dev.classmethod.jp/articles/create-react-router-react-v19-page-routing

さて、この React Router v7 のアプリケーションを Amazon CloudFront + S3 にデプロイする場合は、アプリケーションのビルド時に SPA モードを有効にする必要があります。デフォルトでは SSR モードになるためです。

https://reactrouter.com/how-to/spa

今回は、React v19 + React Router v7 の SPA モードを CloudFront + S3 に AWS CDK でデプロイしてみました。

試してみた

React アプリのビルド

まずは、React アプリケーションを SPA モードを有効にしてビルドします。React アプリケーションのソースコードは、前回のブログで作成したものを使用します。

create-react-app 実行により既定で作成される react-router.config.ts ファイルで ssr オプションを false に設定します。これにより、React Router v7 の SPA モードが有効になります。

my-react-router-app/react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
  // Config options...
  // Server-side render by default, to enable SPA mode set this to `false`
  ssr: false, // SPA モードを有効化する
} satisfies Config;

アプリケーションをビルドすると、build/client ディレクトリにビルド成果物が生成されます。SPA モードなので index.html ファイルが生成され、サーバーサイドのビルド成果物は生成されません。

$ npm run build -w my-react-router-app

> build
> react-router build

vite v6.3.5 building for production...
✓ 48 modules transformed.
build/client/.vite/manifest.json                  2.25 kB │ gzip:  0.48 kB
build/client/assets/logo-dark-pX2395Y0.svg        6.10 kB │ gzip:  2.51 kB
build/client/assets/logo-light-CVbx2LBR.svg       6.13 kB │ gzip:  2.52 kB
build/client/assets/root-D5iR8oEg.css             8.32 kB │ gzip:  2.53 kB
build/client/assets/with-props-Cr-61YSs.js        0.35 kB │ gzip:  0.21 kB
build/client/assets/not-found-BcTa9wDJ.js         0.54 kB │ gzip:  0.35 kB
build/client/assets/home-DDivdm-S.js              0.56 kB │ gzip:  0.35 kB
build/client/assets/root-BWNiOyUc.js              1.13 kB │ gzip:  0.64 kB
build/client/assets/welcome-udj-J-nq.js           3.73 kB │ gzip:  1.72 kB
build/client/assets/chunk-D4RADZKF-Dgj59hC6.js  116.84 kB │ gzip: 39.41 kB
build/client/assets/entry.client-B8EFnelc.js    181.52 kB │ gzip: 57.24 kB
✓ built in 822ms
vite v6.3.5 building SSR bundle for production...
✓ 6 modules transformed.
build/server/.vite/manifest.json               0.23 kB
build/server/assets/server-build-D5iR8oEg.css  8.32 kB
build/server/index.js                          7.98 kB
SPA Mode: Generated build/client/index.html
Removing the server build in /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/cdk_sample_app/my-react-router-app/build/server due to ssr:false
✓ built in 82ms

CDK デプロイ

続いて、AWS CDK を使用して CloudFront + S3 にデプロイします。

lib/main-stack.ts
import * as cdk from "aws-cdk-lib";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as cloudfront_origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3_deployment from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
import * as path from "path";

export class MainStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    // S3 バケット
    const bucket = new s3.Bucket(this, "Bucket", {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // CloudFront Distribution
    const distribution = new cloudfront.Distribution(this, "Distribution", {
      defaultBehavior: {
        origin:
          cloudfront_origins.S3BucketOrigin.withOriginAccessControl(bucket),
      },
      errorResponses: [
        // SPA のページ遷移を正しく処理するための設定
        // @see https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/deploy-a-react-based-single-page-application-to-amazon-s3-and-cloudfront.html#deploy-a-react-based-single-page-application-to-amazon-s3-and-cloudfront-additional
        {
          httpStatus: 403,
          responseHttpStatus: 200,
          responsePagePath: "/index.html",
        },
      ],
    });

    // Distribution のドメイン名を出力
    new cdk.CfnOutput(this, "DistributionDomainName", {
      value: distribution.distributionDomainName,
    });

    // S3 バケットへのコンテンツのデプロイ
    new s3_deployment.BucketDeployment(this, "BucketDeploy", {
      sources: [
        s3_deployment.Source.asset(
          path.join(__dirname, "../my-react-router-app/build/client")
        ),
      ],
      destinationBucket: bucket,
      distribution,
    });
  }
}

BucketDeploymentsources で、先ほどビルドした React アプリケーションの成果物を指定しています。

上記のコードを CDK でデプロイします。

動作確認

デプロイが完了したら、CloudFront のドメイン名を確認してブラウザでアクセスします。

/ にアクセスすると、React アプリケーションのトップページが表示されます。

トップページの Go to Welcome Page リンクをクリックすると、/welcome ページに遷移します。

存在しないパスのページにアクセスすると、あらかじめ作成していた Not Found ページが表示されます。

React Router の SPA モードでのページ遷移が正しく動作していることを確認できました。

うまく動かないパターン

SPA モードを有効にしなかった場合

次のように、react-router.config.ts ファイルで ssr オプションを true に設定したままの場合、サーバーサイドレンダリングが有効になります。

my-react-router-app/react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
  // Config options...
  // Server-side render by default, to enable SPA mode set this to `false`
  ssr: true, // SPA モードを無効化する
} satisfies Config;

このままビルドを行うと、サーバーサイドとクライアントサイドの両方のビルド成果物が生成されます。一方で index.html ファイルは生成されないので、CloudFront + S3 にデプロイしてもうまく動作しない成果物になります。

$ npm run build -w my-react-router-app

> build
> react-router build

vite v6.3.5 building for production...
✓ 48 modules transformed.
build/client/.vite/manifest.json                  2.25 kB │ gzip:  0.48 kB
build/client/assets/logo-dark-pX2395Y0.svg        6.10 kB │ gzip:  2.51 kB
build/client/assets/logo-light-CVbx2LBR.svg       6.13 kB │ gzip:  2.52 kB
build/client/assets/root-D5iR8oEg.css             8.32 kB │ gzip:  2.53 kB
build/client/assets/with-props-Cr-61YSs.js        0.35 kB │ gzip:  0.21 kB
build/client/assets/not-found-BcTa9wDJ.js         0.54 kB │ gzip:  0.35 kB
build/client/assets/home-DDivdm-S.js              0.56 kB │ gzip:  0.35 kB
build/client/assets/root-BWNiOyUc.js              1.13 kB │ gzip:  0.64 kB
build/client/assets/welcome-udj-J-nq.js           3.73 kB │ gzip:  1.72 kB
build/client/assets/chunk-D4RADZKF-Dgj59hC6.js  116.84 kB │ gzip: 39.41 kB
build/client/assets/entry.client-B8EFnelc.js    181.52 kB │ gzip: 57.24 kB
✓ built in 714ms
vite v6.3.5 building SSR bundle for production...
✓ 12 modules transformed.
build/server/.vite/manifest.json                0.58 kB
build/server/assets/logo-dark-pX2395Y0.svg      6.10 kB
build/server/assets/logo-light-CVbx2LBR.svg     6.13 kB
build/server/assets/server-build-D5iR8oEg.css   8.32 kB
build/server/index.js                          14.63 kB
✓ built in 54ms

403 errorResponses の設定が未追加の場合

AWS CDK で errorResponses の設定を追加しないまま CloudFront + S3 にデプロイすると、React Router の SPA モードでのページ遷移が正しく動作しません。

$ git diff
diff --git a/lib/main-stack.ts b/lib/main-stack.ts
index e09f5c9..be5da0e 100644
--- a/lib/main-stack.ts
+++ b/lib/main-stack.ts
@@ -22,15 +22,6 @@ export class MainStack extends cdk.Stack {
         origin:
           cloudfront_origins.S3BucketOrigin.withOriginAccessControl(bucket),
       },
-      errorResponses: [
-        // SPA のページ遷移を正しく処理するための設定
-        // @see https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/deploy-a-react-based-single-page-application-to-amazon-s3-and-cloudfront.html#deploy-a-react-based-single-page-application-to-amazon-s3-and-cloudfront-additional
-        {
-          httpStatus: 403,
-          responseHttpStatus: 200,
-          responsePagePath: "/index.html",
-        },
-      ],
     });

     // Distribution のドメイン名を出力

/ に アクセスすると Access Denied エラーが発生しました。

/index.html にアクセスすると、デフォルトパスのページにリダイレクトされました。

このように、SPA モードでのページ遷移が正しく動作しないため、errorResponses の設定を追加する必要があります。

おわりに

React v19 + React Router v7 の SPA モードを CloudFront + S3 に AWS CDK でデプロイする方法を紹介しました。

どなかたの参考になれば幸いです。

以上

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.