Vite(React Router)で環境変数が無いよってときの対応

先日、React Routerを使ったプロジェクトにてGitHub Actionsでデプロイしたら、テスト環境でエラーが出ました。格闘の末に「ビルド時」と「実行時」の環境変数が区別できていなかったため、備忘録として残しておきます。

この記事で触れているバージョン関係は次のとおりです。

  • Vite v5
  • React Router v7

Viteの環境変数の基本

Viteで扱う環境変数は2種類あります。prefixとしてVITE_が付くかどうかの違いです。

VITE_〇〇〇〇
アクセス方法import.env.VITE_〇〇process.env.〇〇
どこで使われるクライアント側サーバー側
いつ値が決まるかビルド時実行時
コードに埋め込まれるか埋め込まれる埋め込まれない

基本的にViteで扱う環境変数はVITE_をつけて使います。ビルド時に展開されるため、DBのパスワードのような認証情報は書いてはいけません(むしろこういうのを書かないためにprefixを付けるのです)。

一方、React RouterのSSRでloaderのようなサーバーサイドを扱う場合はprefixを付けず、process.env.〇〇のように書きます。こちらは実行時に参照されます。

公式ドキュメント:環境変数とモード | Vite

Google Maps APIはどっちに書けばいいの?

Google Maps APIのようなAPIは、クライアント側で使うことを想定されているため、VITE_の方で問題ありません。ただしリファラ制限(特定のサイトからのみのアクセスに絞る)を忘れないようにしましょう。

リファラ制限のない普通のAPIキーをVITE_で書いてしまうと、コードに埋め込まれて公開されてしまうのでやめましょう。

どこに環境変数を書くの?

普通にシェルでexportをするのは面倒です。Viteでは.envに書くことで環境変数を設定できます。

.env
VITE_FOO=foo-bar-buz

.env.development.env.productionのように環境ごとに用意もできます(環境変数とモード | Vite)。

たとえばGitHub Actionsでデプロイし、ホスティング先をLambda Web Adapterとする場合。
ビルド時に必要なクライアント側の環境変数(VITE_〇〇)は、ワークフロー内で読み込めるようにします(後述)。
実行時に必要なサーバー側の環境変数は、Lambdaの環境変数として設定します。これは手動なりCDKなりで設定します。

ビルド時に埋め込まれるってどういうこと?

VITE_の環境変数がビルド時に埋め込まれる」とはどういうことなのか、具体的に見ていきましょう。

ビルド前のコード
const foo = import.meta.env.VITE_FOO;

環境変数VITE_FOOfoo-bar-buzという値を書いた場合、次のようにビルド後のコードに値が直接書かれています

ビルド後のコード
const foo = "foo-bar-buz";

string型になる

このimport.meta.env.VITE_〇〇はstring型です。12345のような数値を入れても文字列になるため、numberにしたい場合は自分で変換しましょう。

動的に参照できる

import.meta.env.VITE_〇〇ではなくimport.meta.env[key]のように動的に参照できます。

ビルド前のコード
const value = import.meta.env[key]; // keyがstring型の変数

ビルド後のコードを見てみるとimport.meta.envがオブジェクトになっていますね。

ビルド後のコード(整形は筆者によるもの)
const __vite_import_meta_env__ = {
"BASE_URL": "/",
"DEV": false,
"MODE": "production",
"PROD": true,
"SSR": true,
"VITE_MY_NAME": "eetann"
};
const value = __vite_import_meta_env__[key];

環境変数が無かったらエラーにする

上記を踏まえて、環境変数が無ければエラーにする関数を書いてみました。

app/routes/home.tsx
export function getEnvOrThrow(key: string): string {
const value = import.meta.env[key];
if (!value) {
throw new Error(`Environment variable ${key} is not set`);
}
return value;
}
export default function Home() {
return <>{getEnvOrThrow("VITE_MY_NAME") ?? "名前がないよ"}</>;
}

エラーが出たらビルド時の環境変数が正しく設定されたか確認しましょう。単に設定し忘れであったりスペルミスであったり、VITE_の付け忘れなどなど。

npx create-react-router@latest env_checkのようにReact Routerのテンプレートを使ってhome.tsxを書き換えれば試せます。

サーバー側の環境変数チェック

React Routerのloaderのようなサーバーサイドの場合は上記の関数のimport.meta.envの部分をprocess.envに書き換えます。

const value = process.env[key];

GitHub Actionsでのデプロイ時の注意点

GitHub Actionsでデプロイする場合、次のようにビルド時envで渡します。

- name: Build site workspace
env:
VITE_GOOGLE_MAPS_API_KEY: ${{ secrets.VITE_GOOGLE_MAPS_API_KEY }}
VITE_GOOGLE_MAPS_MAP_ID: ${{ secrets.VITE_GOOGLE_MAPS_MAP_ID }}
run: npm run build --workspace=@foo-project/site

環境変数チェック

筆者は次のように「ビルド時に必要な環境変数をチェックするステップ」を入れています。

- name: Validate required secrets
run: |
echo "必須secretsの存在確認を実行中..."
# 必須secretsのチェック
missing_secrets=()
if [ -z "${{ secrets.VITE_GOOGLE_MAPS_API_KEY }}" ]; then
missing_secrets+=("VITE_GOOGLE_MAPS_API_KEY")
fi
if [ -z "${{ secrets.VITE_GOOGLE_MAPS_MAP_ID }}" ]; then
missing_secrets+=("VITE_GOOGLE_MAPS_MAP_ID")
fi
# 不足しているsecretsがある場合はエラー
if [ ${#missing_secrets[@]} -gt 0 ]; then
echo "❌ 以下の必須secretsが設定されていません:"
printf ' - %s\n' "${missing_secrets[@]}"
echo ""
echo "GitHub Secretsまたはローカルの.secrets*ファイルを確認してください。"
exit 1
fi
echo "✅ 全ての必須secretsが設定されています"

別ファイルのワークフローを実行するときは要注意!

secretsを使った別ファイルのワークフローを使う場合、そのワークフローを使う側でもsecretsを渡すのを忘れずに。

jobs:
build:
uses: ./.github/workflows/build-site.yaml
secrets:
VITE_GOOGLE_MAPS_API_KEY: ${{ secrets.VITE_GOOGLE_MAPS_API_KEY }}
VITE_GOOGLE_MAPS_MAP_ID: ${{ secrets.VITE_GOOGLE_MAPS_MAP_ID }}

以上、Viteやそのデプロイ関係の環境変数周りの備忘録でした。「ビルド時」の環境変数なのか、「実行時」の環境変数なのか最初は混乱してました。