Reactで作成したSPAをNetlifyでホストする際のコツ
(2022年11月25日追記)
私の本が株式会社インプレス R&Dさんより出版されました。この記事の内容も含まれています。イラストは鍋料理さんの作品です。猫のモデルはなんとうちのコです!
感想を書いていただけるととても嬉しいです!
(2022年8月3日追記)この記事の内容はこちらの本でも読めます。
はじめに
現在作成中のWebサービスが大分できあがってきたので、Netlifyでホストしてみました。Reactで作ったSPAをNetlifyで動かすのは意外と手間がかかったので、ハマりそうになったところを共有します。今回デプロイしたWebサービスには以下の特徴があります。似たようなことをしようとしている方の参考になれば幸いです。
- Vite + React + TypeScript + TailwindCSSで構築
- React Router v6でルーティング
- CognitoでGoogle認証する
- Amplifyを認証のためだけに使用する
ホスティングサービスとしてNetlifyを選んだのは、ふだんから使い慣れていることと、問い合わせフォームを簡単に設置できるためです。
事前準備
デプロイする前にいろいろと準備が必要でした。
ビルド
Netlifyには静的なファイルしかホストできないので、ビルドしてデプロイ用のファイルを準備します。
npm run build
ビルド結果はdist
ディレクトリに生成されます。実はプロジェクトを開始してから今回始めてビルドしたので、いろいろなエラーが出ました。ビルド時のエラーをひとつひとつ潰していく作業は結構好きなので楽しかったです。npm run dev
で動かしていたときは問題なかったのに、ビルドするとエラーになる事象もあって興味深かったです。エラーメッセージを読めば対処できるものがほとんどでしたが、面倒だった2件だけ以下に記載しておきます。
react-router-hash-linkのエラーに対処する
ページ内遷移させるためにreact-router-hash-linkを使っていたのですが、これはReact Router v4とv5にしか対応していないことがわかりました。最初はソースコードをforkして自力で対応しようかと思いましたが、調べたら同じことをすでにやっていた方がいました。インポート元を@xzar90/react-router-hash-linkに書き換えたらビルドが通るようになりました。
Amplifyのエラーに対処する
ほかのエラーを全部対処したら最後に以下のエラーが残りました。
'request' is not exported by __vite-browser-external, imported by node_modules/@aws-sdk/credential-provider-imds/dist/es/remoteProvider/httpRequest.js
これはViteとAmplifyの組み合わせで発生するエラーのようで、こちらのIssueを参考に対応しました。vite.config.ts
を以下のように書き換えたらビルドが通るようになりました。
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
+ resolve: {
+ alias: {
+ './runtimeConfig': './runtimeConfig.browser',
+ },
+ },
});
ビルド後の動作確認
ビルドが成功したら、dist
に生成されたファイルの動作確認を行います。私はよく下記のコマンドで簡易HTTPサーバーを立てて静的HTMLの動作確認をしますが、SPAの場合はこの方法では問題があります。
cd dist
python3 -m http.server {ポート番号}
ルートにアクセスしたり、リンクをクリックして画面遷移する分には良いのですが、URLを指定してルート以外にアクセスすると404エラーになってしまいます。dist
にはルートにしかindex.htmlが存在しないので、考えてみたら当たり前ですね。ではなぜnpm run dev
で動かしていたときはURLを直接指定しても正常に表示されていたかというと、ルート以外へのアクセスをテストサーバーがルートにリダイレクトしてくれていたからです。したがってdist
のテストをするときは、すべてのアクセスをルートにリダイレクトしてやれば開発時の動きを再現できます。Pythonでもできますがやや面倒なので、お手軽に確認したい場合はNode.jsのservor
を利用するとよいでしょう。カレントディレクトリを3000番ポートで確認したい場合は以下のようにします。
npx servor . index.html 3000
引数の意味は以下の通りです。
servor {ルートディレクトリ} {ファイル名} {ポート番号}
Netlify用のリダイレクト設定を追加
上記の問題はNetlifyにデプロイしたときも発生します。Netlifyですべてのアクセスをルートのindex.htmlにリダイレクトするのは簡単で、以下の内容のnetlify.toml
をReactプロジェクトのルートに配置しておくだけです。
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Amplifyの設定情報を環境変数に移動
今まではAmplifyの設定をawsExports.ts
にベタ書きしてローカルのみで管理していましたが、この方法ではGitHubリポジトリとNetlifyの連動に支障をきたします。そこでawsExports.ts
はリポジトリ上で管理することにして、秘密にしたい値は環境変数に追い出します。新たなawsExports.ts
は次のようにしました。
// Amplifyの設定
const awsExports = {
Auth: {
region: import.meta.env.VITE_REGION,
userPoolId: import.meta.env.VITE_USER_POOL_ID,
userPoolWebClientId: import.meta.env.VITE_USER_POOL_WEB_CLIENT_ID,
oauth: {
domain: import.meta.env.VITE_OAUTH_DOMAIN,
scope: ['openid'],
redirectSignIn: import.meta.env.VITE_OAUTH_REDIRECT_SIGN_IN,
redirectSignOut: import.meta.env.VITE_OAUTH_REDIRECT_SIGN_OUT,
responseType: 'code',
},
},
};
export default awsExports;
Viteで扱う環境変数は、このようにVITE_
を接頭語にする必要があります。
ローカル環境の環境変数は.env.local
に書いておくとよいでしょう。.env.local
はViteではデフォルトでgitignoreの対象になっています。
VITE_REGION=ap-northeast-1
VITE_USER_POOL_ID=ap-northeast-1_XXXXXXXXX
VITE_USER_POOL_WEB_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXXXX
VITE_OAUTH_DOMAIN=XXXXXXXXXXXXXXXXXXXXXXX.amazoncognito.com
VITE_OAUTH_REDIRECT_SIGN_IN=http://localhost:3000/mypage
VITE_OAUTH_REDIRECT_SIGN_OUT=http://localhost:3000/
また、TypeScriptのエラーが出るので、vite-env.d.ts
を以下のように修正しておきます。
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_REGION: string;
readonly VITE_USER_POOL_ID: string;
readonly VITE_USER_POOL_WEB_CLIENT_ID: string;
readonly VITE_OAUTH_DOMAIN: string;
readonly VITE_OAUTH_REDIRECT_SIGN_IN: string;
readonly VITE_OAUTH_REDIRECT_SIGN_OUT: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
Netlifyにデプロイする
準備ができたらNetlifyにサインインして、GitHubのリポジトリを連携します。ビルドの設定は下図のようにします。
環境変数はボタンをクリックしてAdvanced build setting
を開くと設定できます。
VITE_OAUTH_REDIRECT_SIGN_IN
とVITE_OAUTH_REDIRECT_SIGN_OUT
はhttp://localhost:3000
ではなくNetlifyのURLを指定する必要があるので注意しましょう。
Cognitoユーザープールの設定変更
Cognitoのユーザープールを開き、「ホストされたサインアップページとサインインページ」にNetlifyのURLを追加します。これでNetlifyにホストした環境でも正常にサインインできるようになります。
問い合わせフォームを有効にする
問い合わせフォームはきちんと作ろうとすると意外に面倒です。Netlifyは面倒な問い合わせフォームを簡単に実装できるのが魅力のひとつとなっています。ただしReactから利用する場合は若干の手間がかかります。
ダミーのHTMLファイルが必要
NetlifyはホストされたHTMLファイルを解析し、条件に合致する<form>
タグを見つけたら問い合わせフォーム機能を有効にします。ReactでビルドされたファイルはほとんどがJavaScriptでできているため、単純に<form>
を実装しただけではNetlifyが認識できません。そこで、Netlifyにフォームの存在を教えるためのダミーのHTMLファイルを用意する必要があります。この方法は公式ドキュメントや公式ブログに載っています。ダミーのHTMLファイルはプロジェクト直下のpublic
ディレクトリに格納します。
<!-- NetlifyのFormを利用するための静的HTML -->
<form
name="contact"
data-netlify="true"
netlify-honeypot="bot-field"
hidden
>
<input type="text" name="name" />
<input type="text" name="company" />
<input type="email" name="email" />
<input type="text" name="name" />
<textarea name="message"></textarea>
</form>
data-netlify="true"
がポイントで、<form>
タグにこのオプションを追加することでNetlifyの問い合わせフォーム機能が有効になります。
Reactで実装された問い合わせフォームはこのようになっています。<input>
タグのname
はdummy_form.html
に合わせる必要があります。
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { FC } from 'react';
import { HashLink } from '@xzar90/react-router-hash-link';
const Contact: FC = () => (
<section id="Contact" className="bg-gray-100 py-6 sm:py-8 lg:py-12">
<div className="mx-auto max-w-screen-2xl px-4 md:px-8">
<div className="mb-10 md:mb-16">
<h2 className="mb-4 text-center text-2xl font-bold text-gray-800 md:mb-6 lg:text-3xl">
お問い合わせ
</h2>
</div>
<form
className="mx-auto grid max-w-screen-md gap-4"
name="contact"
method="POST"
data-netlify="true"
>
<div className="sm:col-span-2">
<label htmlFor="name" className="text-sm text-gray-800 sm:text-base">
お名前<span className="required-dot text-red-500">*</span>
<input
name="name"
type="text"
className="w-full rounded border bg-gray-50 px-3 py-2 text-gray-800 outline-none ring-indigo-300 transition duration-100 focus:ring"
/>
</label>
</div>
<div className="sm:col-span-2">
<label
htmlFor="company"
className="text-sm text-gray-800 sm:text-base"
>
会社名
<input
name="company"
type="text"
className="w-full rounded border bg-gray-50 px-3 py-2 text-gray-800 outline-none ring-indigo-300 transition duration-100 focus:ring"
/>
</label>
</div>
<div className="sm:col-span-2">
<label htmlFor="email" className="text-sm text-gray-800 sm:text-base">
メールアドレス<span className="required-dot text-red-500">*</span>
<input
name="email"
type="email"
className="w-full rounded border bg-gray-50 px-3 py-2 text-gray-800 outline-none ring-indigo-300 transition duration-100 focus:ring"
/>
</label>
</div>
<div className="sm:col-span-2">
<label
htmlFor="message"
className="text-sm text-gray-800 sm:text-base"
>
お問い合わせ内容
<span className="required-dot text-red-500">*</span>
<textarea
name="message"
className="h-64 w-full rounded border bg-gray-50 px-3 py-2 text-gray-800 outline-none ring-indigo-300 transition duration-100 focus:ring"
/>
</label>
</div>
<input type="hidden" name="form-name" value="contact" />
<div className="flex items-center justify-between sm:col-span-2">
<button
type="submit"
className="inline-flex rounded border-0 bg-blue-500 py-3 px-6 text-lg text-white hover:bg-blue-600 focus:outline-none active:bg-blue-700"
>
送信
</button>
</div>
<p className="text-xs text-gray-400">
送信完了をもって
<HashLink
to="/privacy_policy#top"
className="underline transition duration-100 hover:text-indigo-500 active:text-indigo-600"
>
プライバシーポリシー
</HashLink>
に同意したものとみなします。
</p>
</form>
</div>
</section>
);
export default Contact;
これでNetlifyが問い合わせフォームを認識できるようになりました。Netlify設定のForm notificationsでメールやSlackと連携させましょう。
サンクスページを表示したい
問い合わせフォームは動くようになりましたが、実際に送信するとNetlifyのデフォルトサンクスページが表示されてしまいます。自前のサンクスページに遷移させたいのですが、これが意外と難しいです。通常のHTMLなら<form>
タグにaction="/thanks"
などと設定するだけですが、Reactで同じことをすると404エラーになってしまいます。メカニズムはよくわかりませんが、actionによる遷移ではルートへの自動リダイレクトが効かないのでしょうか?この問題の解決方法をネットで調べましたが、見つかった方法は複雑な上にうまくいかなかった(一瞬サンクスページが表示されるが、結局デフォルトのページが表示されてしまった)ので、自分で頭を絞った結果、次の方法で解決しました。
まず、サンクスページに転送するだけの機能を持ったダミーのサンクスページを用意します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- 本当のthanksページに遷移させる -->
<meta http-equiv="refresh" content="0;url=/thanks" />
<title>FM Mail</title>
</head>
<body>
<br />
</body>
</html>
次に、ダミーフォームとReactで実装した問い合わせフォームの両方にaction="/dummy_thanks.html"
を追加します。実験したところ、どちらか片方だけでは正常に機能しなかったので、両方に同じ記述を追加するのがポイントのようです。
<!-- NetlifyのFormを利用するための静的HTML -->
<form
name="contact"
data-netlify="true"
netlify-honeypot="bot-field"
+ action="/dummy_thanks.html"
hidden
>
<input type="text" name="name" />
<input type="text" name="company" />
<input type="email" name="email" />
<input type="text" name="name" />
<textarea name="message"></textarea>
</form>
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { FC } from 'react';
import { HashLink } from '@xzar90/react-router-hash-link';
const Contact: FC = () => (
<section id="Contact" className="bg-gray-100 py-6 sm:py-8 lg:py-12">
<div className="mx-auto max-w-screen-2xl px-4 md:px-8">
<div className="mb-10 md:mb-16">
<h2 className="mb-4 text-center text-2xl font-bold text-gray-800 md:mb-6 lg:text-3xl">
お問い合わせ
</h2>
</div>
<form
className="mx-auto grid max-w-screen-md gap-4"
name="contact"
method="POST"
data-netlify="true"
+ action="/dummy_thanks.html"
>
// 後略
dummy_thanks.html
は実ファイルなので404になることはありません。そして<meta http-equiv="refresh" content="0;url=/thanks" />
で/thanks
に遷移した場合はnetlify.toml
で設定したルートへのリダイレクトが効くため、React Routerで設定したサンクスページが正常に表示されるという仕組みです。
まとめ
以下の内容をご紹介しました。
- Cognito認証やReact Routerを利用したReactのSPAをNetlifyにデプロイする方法
- ReactでNetlifyフォームを利用する方法
- React + Netlifyフォームでサンクスページを表示するための工夫
問い合わせフォームの実装では調査に思いがけず時間がかかってしまい、途中から自分で実装した方が早かったかもしれないと思ったりもしましたが、きっと次回からはスムーズに実装できるはずです。Netlifyは日本国内にCDNがないので若干遅いのが難点ですが、それ以外は非常に良いサービスだと思います。
Discussion