- React
Reactの環境変数.envの使い方|VITE_がundefinedの原因
React(Vite)の環境変数とは、プロジェクト直下の .env ファイルに「VITE_」で始まる名前で書いた値を、コードから import.meta.env.VITE_名前 で読む仕組みです。 値が undefined になるときは、ほとんどが「名前が VITE_ で始まっていない」「.env の置き場所が違う」「process.env で読んでいる」のどれかに当てはまります。
この記事では、同じ変数を4種類の .env ファイルに書き、開発サーバーとビルドで実際に出力した結果から優先順位を表にしました。あわせて、undefined になる原因を1つずつ再現し、出力と直し方を並べています。上から順に確かめれば、どこでつまずいているかを切り分けられる構成です。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / Vite 8.3.2 / React 19.3.0 / @vitejs/plugin-react 6.1.1 / TypeScript 7.0.2。画面の出力は Playwright 1.56.0 の Chromium で取得しています。
React(Vite)の環境変数の早見表
| やりたいこと | 書き方 | 補足 |
|---|---|---|
| 変数を定義する | プロジェクト直下の .env に VITE_API_URL=https://api.example.com |
名前は VITE_ で始める |
| コードで読む | import.meta.env.VITE_API_URL |
値はすべて文字列 |
| 自分のPCだけの値にする | .env.local に書く |
.env より優先される |
| 開発とビルドで値を変える | .env.development と .env.production |
モードごとに読み込まれる |
| 検証用など第3の設定を作る | .env.staging を作り vite build --mode staging |
モード名は自由に付けられる |
| いまのモードを知る | import.meta.env.MODE |
DEV と PROD は真偽値 |
| HTMLに埋め込む | index.html に %VITE_APP_TITLE% |
見つからない名前はそのまま残る |
| vite.config の中で読む | loadEnv(mode, process.cwd()) |
設定ファイルでは import.meta.env が使えない |
| 型と補完を付ける | src/vite-env.d.ts に ImportMetaEnv を定義 |
打ち間違いを tsc が教えてくれる |
| 真偽値として使う | import.meta.env.VITE_FLAG === "true" |
"false" は文字列なので真になる |
読み込みの優先順位(今回の実行結果)は次のとおりです。右にあるものほど優先されます。
| 順位(低→高) | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 読み込み元 | .env |
.env.local |
.env.[mode] |
.env.[mode].local |
コマンド実行時にすでにある環境変数 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Reactの環境変数.envの使い方|VITE_がundefinedの原因」
いちばん小さな完成形で .env の使い方を確かめる
まずは、動くひとそろいを用意します。フォルダ構成は次のとおりです。.env は package.json と同じ階層(プロジェクト直下)に置きます。
my-app/
├── .env
├── index.html
├── package.json
├── vite.config.ts
└── src/
├── main.tsx
└── App.tsx
インストールするパッケージは次の4つです。
npm install react react-dom
npm install -D vite @vitejs/plugin-react
# .env
VITE_APP_TITLE=備品管理
VITE_API_URL=https://api.example.com/v1
SECRET_KEY=abc123
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});
<!-- index.html -->
<!doctype html>
<html lang="ja">
<head><meta charset="UTF-8" /><title>%VITE_APP_TITLE%</title></head>
<body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body>
</html>
// src/main.tsx
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")!).render(<App />);
// src/App.tsx
export default function App() {
const env = import.meta.env;
const rows = {
MODE: env.MODE,
DEV: String(env.DEV),
PROD: String(env.PROD),
BASE_URL: env.BASE_URL,
VITE_APP_TITLE: env.VITE_APP_TITLE,
VITE_API_URL: env.VITE_API_URL,
SECRET_KEY: String(env.SECRET_KEY),
};
return <pre id="out">{JSON.stringify(rows, null, 2)}</pre>;
}
npx vite で開発サーバーを起動してページを開くと、タイトルと本文に .env の値が表示されました。
title: 備品管理
{
"MODE": "development",
"DEV": "true",
"PROD": "false",
"BASE_URL": "/",
"VITE_APP_TITLE": "備品管理",
"VITE_API_URL": "https://api.example.com/v1",
"SECRET_KEY": "undefined"
}
ここで押さえたい点は2つです。
- VITE_ で始まる変数だけがブラウザ側のコードに渡ります。
SECRET_KEYは .env に書いてあっても undefined でした。 - MODE・DEV・PROD・BASE_URL は Vite が最初から用意している値です。.env に書かなくても読めます。公式ドキュメントには、このほかにサーバー側で動いているかを表す SSR も載っています。
Vite が最初から用意している値
| 名前 | 型 | 内容 | 開発サーバーでの値 | ビルド後の値 |
|---|---|---|---|---|
import.meta.env.MODE |
文字列 | いまのモード名 | development |
production |
import.meta.env.DEV |
真偽値 | 開発サーバーで動いているか | true |
false |
import.meta.env.PROD |
真偽値 | ビルドした成果物で動いているか | false |
true |
import.meta.env.BASE_URL |
文字列 | 設定の base の値 | / |
/ |
.env ファイル4種類の優先順位を実際に出力して確かめる
Vite は次の4種類のファイルを読み込みます。[mode] の部分には、開発サーバーなら development、ビルドなら production が入ります。
| ファイル | 読み込まれる場面 | Git での扱いの目安 |
|---|---|---|
.env |
いつも | 共有してよい既定値を書いてコミットする |
.env.local |
いつも | 自分のPCだけの値。コミットしない |
.env.[mode] |
そのモードのときだけ | モードごとの既定値を書いてコミットする |
.env.[mode].local |
そのモードのときだけ | 自分のPCだけの値。コミットしない |
公式ドキュメントは、.local で終わるファイルを Git に入れないよう、.gitignore に *.local を書くことを案内しています。
同じ変数を4ファイルに書いて比べる
どれが優先されるかを見るために、どのファイルに書いたかがわかる値を入れます。VITE_A は .env だけ、VITE_B は2ファイル、VITE_C は3ファイル、VITE_D は4ファイルすべてに書きました。
# .env
VITE_A=A:.env
VITE_B=B:.env
VITE_C=C:.env
VITE_D=D:.env
VITE_SHELL=SHELL:.env
# .env.local
VITE_B=B:.env.local
VITE_C=C:.env.local
VITE_D=D:.env.local
VITE_SHELL=SHELL:.env.local
# .env.development
VITE_C=C:.env.development
VITE_D=D:.env.development
VITE_SHELL=SHELL:.env.development
# .env.development.local
VITE_D=D:.env.development.local
VITE_SHELL=SHELL:.env.development.local
.env.production/.env.production.local と、.env.staging/.env.staging.local も、ファイル名の部分だけ変えた同じ内容で用意しました。App.tsx の表示項目を VITE_A〜VITE_D と VITE_SHELL に変え、次の3通りで出力を取りました。
| 実行したコマンド | MODE | DEV | PROD |
|---|---|---|---|
npx vite(開発サーバー) |
development | true | false |
npx vite build のあと npx vite preview |
production | false | true |
npx vite build --mode staging のあと npx vite preview |
staging | false | true |
優先順位の実行結果
| 変数 | 書いたファイル | 開発サーバー | build | build --mode staging |
|---|---|---|---|---|
| VITE_A | .env だけ |
A:.env | A:.env | A:.env |
| VITE_B | .env と .env.local |
B:.env.local | B:.env.local | B:.env.local |
| VITE_C | 上の2つと .env.[mode] |
C:.env.development | C:.env.production | C:.env.staging |
| VITE_D | 4ファイルすべて | D:.env.development.local | D:.env.production.local | D:.env.staging.local |
この結果から、次のことがわかります。
- .env.local は .env より優先されます(VITE_B)。
- モード別のファイルは .env.local より優先されます(VITE_C)。「.local が付いていれば常に最優先」ではない点が、迷いやすいところです。
- 4つとも書いた場合は .env.[mode].local が採用されます(VITE_D)。
- ビルドでは .env.development 系は読まれず、.env.production 系に切り替わります。
npx vite --mode staging のように開発サーバーへモードを渡した場合は、MODE が staging、DEV が true のまま、.env.staging 系の値が読まれました。モード(どの .env を読むか)と、DEV/PROD(開発サーバーかビルドか)は別々に決まります。
コマンドの前に書いた環境変数がいちばん強い
VITE_SHELL は4ファイルすべてに書いたうえで、コマンドの前にも値を付けて起動しました。
VITE_SHELL=SHELL:shell npx vite
"VITE_SHELL": "SHELL:shell"
VITE_SHELL=SHELL:shell npx vite build でも、成果物には SHELL:shell が入りました。コマンドを実行する時点ですでにある環境変数は、どの .env ファイルよりも優先されます。 CI(自動ビルドの仕組み)の設定画面で登録した変数が .env より優先されるのも、この順位によるものです。
flowchart LR
A[".env"] --> B[".env.local"] --> C[".env.[mode]"] --> D[".env.[mode].local"] --> E["実行時にすでにある環境変数"]
E --> F["import.meta.env に入る値"]
import.meta.env が undefined になる原因チェックリスト
値が読めないときは、次の表を上から順に確かめます。右端は、今回それぞれを再現したときの出力です。
| 番号 | 確かめること | 起きる症状(今回の出力) | 直し方 |
|---|---|---|---|
| 1 | 変数名が VITE_ で始まっているか |
undefined | 名前を VITE_ で始める |
| 2 | .env がプロジェクト直下にあるか | undefined | package.json と同じ階層に置く |
| 3 | process.env で読んでいないか |
開発サーバーで ReferenceError、ビルド後は undefined | import.meta.env に変える |
| 4 | vite.config の中で読んでいないか | undefined | loadEnv を使う |
| 5 | 変更後にサーバーが読み直したか | 追加前の値のまま | ログの再起動表示を確かめる。シェルの変数を変えたら起動し直す |
| 6 | モードに合う .env に書いたか | 別ファイルの値、または undefined | import.meta.env.MODE を表示して確かめる |
| 7 | 値に # や空白が入っていないか |
途中で切れる | 値を二重引用符で囲む |
| 8 | 変数名の打ち間違いがないか | undefined | 型定義を置いて tsc で見つける |
以下、番号順に再現コードと出力を載せます。再現には次のコンポーネントを使いました。エラーも文字列として表示できるよう、関数で包んでいます。
// src/App.tsx
function safe(fn: () => unknown) {
try {
return String(fn());
} catch (e) {
return (e as Error).name + ": " + (e as Error).message;
}
}
export default function App() {
const key = "VITE_A";
const rows = {
"1 接頭辞なし SECRET_KEY": safe(() => import.meta.env.SECRET_KEY),
"2 src/.env の VITE_IN_SRC": safe(() => import.meta.env.VITE_IN_SRC),
"3 process.env.VITE_A": safe(() => process.env.VITE_A),
"動的キー import.meta.env[key]": safe(() => import.meta.env[key]),
};
return <pre id="out">{JSON.stringify(rows, null, 2)}</pre>;
}
開発サーバー(npx vite)での出力です。
{
"1 接頭辞なし SECRET_KEY": "undefined",
"2 src/.env の VITE_IN_SRC": "undefined",
"3 process.env.VITE_A": "ReferenceError: process is not defined",
"動的キー import.meta.env[key]": "A:.env"
}
ビルド後(npx vite build のあと npx vite preview)の出力です。
{
"1 接頭辞なし SECRET_KEY": "undefined",
"2 src/.env の VITE_IN_SRC": "undefined",
"3 process.env.VITE_A": "undefined",
"動的キー import.meta.env[key]": "A:.env"
}
原因1:変数名が VITE_ で始まっていない
.env に SECRET_KEY=abc123 と書いても、ブラウザ側では undefined です。Vite は、VITE_ で始まる変数だけをブラウザ向けのコードに渡します。うっかり秘密の値が画面側に出ていかないようにするための仕組みです。
# 直す前
API_URL=https://api.example.com/v1
# 直した後
VITE_API_URL=https://api.example.com/v1
接頭辞を変えたい、または増やしたいときは、vite.config の envPrefix で指定できます。次の設定で、APP_ で始まる変数も読めることを確かめました。
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
envPrefix: ["VITE_", "APP_"],
});
"APP_NAME": "prefix-APP"
原因2:.env の置き場所が違う
src フォルダの中に .env を置き、VITE_IN_SRC=src-no-naka と書いた場合、値は undefined でした。Vite が .env を探すのはプロジェクト直下(vite.config がある階層)です。
別のフォルダにまとめたいときは envDir を指定します。config フォルダに .env を置いて次の設定にすると、そのフォルダの値が読めました。
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
envDir: "./config",
});
"VITE_FROM_CONFIG": "config-dir",
"VITE_A": "undefined"
envDir を指定すると、プロジェクト直下の .env は読まれなくなります(直下にだけ書いた VITE_A が undefined になりました)。置き場所は1か所にそろえるのがわかりやすいです。
原因3:process.env で読んでいる
process.env.VITE_A と書いた場合、開発サーバーでは次のエラーになりました。
ReferenceError: process is not defined
process は Node.js が持っているオブジェクトで、ブラウザにはありません。一方、ビルド後は同じコードがエラーにならず、undefined を返しました。成果物を見ると、process.env の部分が空のオブジェクトに置き換わっています。
"3 process.env.VITE_A":p(()=>({}).VITE_A)
開発中はエラーで気づけますが、ビルド後は静かに undefined になるので、原因を探しにくくなります。Vite では import.meta.env に統一すると覚えておくと安心です。
// 直す前
const url = process.env.VITE_API_URL;
// 直した後
const url = import.meta.env.VITE_API_URL;
原因4:vite.config の中で読んでいる
vite.config.ts は、Vite が .env を読み込む前に Node.js で実行されます。そのため、設定ファイルの中では process.env にも import.meta.env にも .env の値が入っていません。設定ファイルで .env の値を使うときは loadEnv を呼びます。
// vite.config.ts
import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig(({ mode }) => {
console.log("process.env.VITE_A =", process.env.VITE_A);
console.log("import.meta.env =", typeof import.meta.env);
const env = loadEnv(mode, process.cwd());
console.log("loadEnv(mode, cwd) =", env.VITE_A, "/ SECRET_KEY:", env.SECRET_KEY);
const all = loadEnv(mode, process.cwd(), "");
console.log("loadEnv(mode, cwd, '') =", all.VITE_A, "/ SECRET_KEY:", all.SECRET_KEY);
return { plugins: [react()] };
});
npx vite build を実行したときの出力です。
process.env.VITE_A = undefined
import.meta.env = undefined
loadEnv(mode, cwd) = A:.env / SECRET_KEY: undefined
loadEnv(mode, cwd, '') = A:.env / SECRET_KEY: abc123
| 書き方 | 得られる値 |
|---|---|
process.env.VITE_A |
undefined(.env はまだ読まれていない) |
import.meta.env |
undefined(設定ファイルでは使えない) |
loadEnv(mode, process.cwd()) |
VITE_ で始まる変数だけ |
loadEnv(mode, process.cwd(), "") |
接頭辞なしの変数も含むすべて |
第3引数に空文字を渡すと、SECRET_KEY のような接頭辞なしの変数も読めます。開発サーバーの proxy の転送先など、設定ファイルの中だけで使う値は VITE_ を付けずに書き、loadEnv で読む形にすると、ブラウザ側には渡りません。
原因5:.env を変えたあとに読み直されていない
Vite は起動時に .env を読み込みます。Vite 8.3.2 で、開発サーバーを動かしたまま .env に1行追加したところ、サーバーが自動で再起動しました。
# 起動中に追記した行
VITE_ADDED=あとから追加
VITE v8.3.2 ready in 202 ms
➜ Local: http://127.0.0.1:8881/
11:16:43 PM [vite] .env changed, restarting server...
11:16:44 PM [vite] server restarted.
追記の前は undefined、追記の後にページを開き直すと「あとから追加」と表示されました。.env を保存したら、ターミナルに「.env changed, restarting server...」が出ているかを見るのが確実です。
次の場合は自動では反映されないので、Ctrl+C で止めて起動し直します。
| 変えたもの | 反映のしかた |
|---|---|
| プロジェクト直下の .env 系ファイル | 自動で再起動(上のログ) |
| コマンドの前やシェルで設定した環境変数 | 開発サーバーを起動し直す |
| ビルド済みの dist | もう一度 npx vite build を実行する |
ビルド済みの成果物には値が文字として埋め込まれているため、.env を書き換えただけでは変わりません(後の章で確かめます)。
原因6:モードに合うファイルに書いていない
.env.production にだけ書いた変数は、開発サーバーでは読めません。逆も同じです。迷ったら、まず import.meta.env.MODE を画面に出して、いまどのモードかを確かめます。
| 症状 | 見るところ |
|---|---|
| 開発では読めるがビルド後に undefined | .env.development にだけ書いていないか |
| ビルド後だけ別の値になる | .env.production や .env.production.local に同じ名前がないか |
| 自分のPCだけ値が違う | .env.local や .env.[mode].local に同じ名前がないか |
| CI でだけ値が違う | CI に登録した環境変数(.env より優先)がないか |
原因7:値に # や空白が入っている
.env では、# から後ろがコメントとして扱われます。値の途中に # を含めたいときは、二重引用符で囲みます。
VITE_NOTE=色 #1 の説明
VITE_NOTE_Q="色 #1 の説明"
"VITE_NOTE": "色",
"VITE_NOTE_Q": "色 #1 の説明"
また、値の中で $名前 と書くと、ほかの変数の値に展開されます。
VITE_API_HOST=https://api.example.com
VITE_API_URL=$VITE_API_HOST/v1
"VITE_API_URL": "https://api.example.com/v1"
公式ドキュメントによると、$ そのものを値に含めたいときは \$ とバックスラッシュを付けて書きます。
動的なキーで読むとどうなるか
import.meta.env[key] のように、変数に入れた名前で読む書き方も確かめました。Vite 8.3.2 では、開発サーバーでもビルド後でも値が読めました(上の出力の「動的キー」の行)。
ただし、この書き方をすると、成果物に入る内容が変わります。次の章で、distの中身を見ながら説明します。
ビルド後の dist に値が埋め込まれることを確かめる
環境変数は、ビルドのときに値そのものがJavaScriptファイルへ書き込まれます。ブラウザが .env を読みに行くわけではありません。
1つの変数だけを読むコードの場合
// src/App.tsx
export default function App() {
return <pre id="out">{import.meta.env.VITE_A}</pre>;
}
npx vite build のあと、成果物を検索しました。
grep -o '.\{30\}A:\.env.\{20\}' dist/assets/*.js
grep -c 'B:.env.local' dist/assets/*.js
sx)(`pre`,{id:`out`,children:`A:.env`})}(0,d.createRoot)
0
import.meta.env.VITE_A と書いた場所が、文字列 A:.env に置き換わっています。使っていない VITE_B の値は成果物に入っていません。
import.meta.env をまるごと使うコードの場合
const env = import.meta.env; のようにオブジェクトごと変数に入れたり、動的なキーで読んだりすると、VITE_ で始まる変数がすべて成果物に入ります。
grep -o 'VITE_[A-Z_]*:`[^`]*`' dist/assets/*.js
grep -c "abc123" dist/assets/*.js
VITE_A:`A:.env`
VITE_API_HOST:`https://api.example.com`
VITE_API_URL:`https://api.example.com/v1`
VITE_APP_TITLE:`備品管理`
VITE_B:`B:.env.local`
VITE_C:`C:.env.production`
VITE_D:`D:.env.production.local`
VITE_FEATURE_NEW_UI:`false`
VITE_NOTE:`色`
VITE_NOTE_Q:`色 #1 の説明`
VITE_PAGE_SIZE:`20`
VITE_SHELL:`SHELL:.env.production.local`
0
最後の 0 は、SECRET_KEY=abc123 の値が成果物に1件も含まれていないことを示しています。VITE_ を付けない変数はビルド結果に入りません。
この結果からわかる使い分け
| 置きたい値 | VITE_ を付けるか | 理由 |
|---|---|---|
| APIのURL、画面タイトル、機能の切り替えフラグ | 付ける | 見られても困らない値 |
| 公開前提のキー(ブラウザから使う前提で発行されたもの) | 付ける | 利用元の制限などをサービス側で設定する |
| パスワード、秘密鍵、管理用のトークン | 付けない | 成果物を開けば誰でも読めるため、サーバー側で持つ |
公式ドキュメントも、VITE_ で始まる変数には秘密の情報を入れないよう案内しています。「VITE_ を付けた値は、配布するファイルに書かれて公開される」と考えておくと判断しやすくなります。
また、値はビルドの時点で確定します。ビルドした後に環境変数を変えても、成果物の値は変わりません。 環境ごとに値を変えるときは、環境ごとにビルドし直します。
index.html で環境変数を使う
index.html では、%名前% の形で環境変数を埋め込めます。
<title>%VITE_APP_TITLE% / %VITE_MISSING%</title>
VITE_APP_TITLE は定義あり、VITE_MISSING は定義なしの状態でビルドすると、次の警告が出ました。
(!) %VITE_MISSING% is not defined in env variables found in /index.html. Is the variable mistyped?
<title>備品管理 / %VITE_MISSING%</title>
JavaScript では undefined になりますが、HTMLでは見つからない名前がそのまま文字として残ります。画面に %...% が見えたら、変数名か .env の中身を見直す合図です。
ImportMetaEnv の型定義で打ち間違いを見つける
TypeScript では、型定義を1ファイル置くだけで、補完が効き、打ち間違いをビルド前に見つけられます。
まず、型定義を置かない状態で、わざと打ち間違いを入れたコードを用意します。
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"types": ["vite/client"]
},
"include": ["src"]
}
// src/App.tsx
export default function App() {
const title = import.meta.env.VITE_APP_TITLE;
const typo = import.meta.env.VITE_APP_TITEL;
const size: number = import.meta.env.VITE_PAGE_SIZE;
return <pre id="out">{title}{typo}{size}</pre>;
}
型定義なしで npx tsc --noEmit を実行すると、エラーは0件でした。vite/client の既定の型では、どんな名前でも読める(型は any)ようになっているためです。打ち間違いの VITE_APP_TITEL も、数値型への代入も通ります。
手順1:使う変数を ImportMetaEnv に書く
// src/vite-env.d.ts
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string;
readonly VITE_API_URL: string;
readonly VITE_PAGE_SIZE: string;
readonly VITE_FEATURE_NEW_UI: "true" | "false";
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
src/App.tsx(4,9): error TS2322: Type 'string' is not assignable to type 'number'.
VITE_PAGE_SIZE が文字列であることを tsc が教えてくれるようになりました。ただし、この段階では打ち間違いの VITE_APP_TITEL はまだ通ります。
手順2:strictImportMetaEnv で未定義の名前をエラーにする
型定義の先頭に ViteTypeOptions を足します。
// src/vite-env.d.ts
interface ViteTypeOptions {
strictImportMetaEnv: unknown;
}
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string;
readonly VITE_API_URL: string;
readonly VITE_PAGE_SIZE: string;
readonly VITE_FEATURE_NEW_UI: "true" | "false";
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
src/App.tsx(3,32): error TS2551: Property 'VITE_APP_TITEL' does not exist on type 'ImportMetaEnv'. Did you mean 'VITE_APP_TITLE'?
src/App.tsx(4,9): error TS2322: Type 'string' is not assignable to type 'number'.
打ち間違いに対して「VITE_APP_TITLE のことですか」と候補まで出ます。 undefined の原因が名前の間違いだった、という場面をビルド前に減らせます。
| 状態 | 打ち間違い(VITE_APP_TITEL) | 数値型への代入 |
|---|---|---|
| 型定義なし | 通る | 通る |
| ImportMetaEnv を定義 | 通る | TS2322 |
| strictImportMetaEnv も追加 | TS2551(候補つき) | TS2322 |
公式ドキュメントには、この型定義ファイルに import 文を書くと型の拡張が効かなくなる、という注意があります。vite-env.d.ts には import を書かないようにします。
文字列 "false" が真になる注意と、数値の扱い
.env の値は、すべて文字列としてコードに届きます。真偽値や数値のつもりで書いても、型は string です。
VITE_FEATURE_NEW_UI=false
VITE_PAGE_SIZE=20
// src/App.tsx
export default function App() {
const flag = import.meta.env.VITE_FEATURE_NEW_UI;
const rows = {
"VITE_FEATURE_NEW_UI": flag,
"typeof": typeof flag,
"if (flag)": flag ? "真(新UIを表示)" : "偽",
"flag === 'true'": String(flag === "true"),
"Number(VITE_PAGE_SIZE) + 1": String(Number(import.meta.env.VITE_PAGE_SIZE) + 1),
"VITE_PAGE_SIZE + 1": import.meta.env.VITE_PAGE_SIZE + 1,
};
return <pre id="out">{JSON.stringify(rows, null, 2)}</pre>;
}
{
"VITE_FEATURE_NEW_UI": "false",
"typeof": "string",
"if (flag)": "真(新UIを表示)",
"flag === 'true'": "false",
"Number(VITE_PAGE_SIZE) + 1": "21",
"VITE_PAGE_SIZE + 1": "201"
}
VITE_FEATURE_NEW_UI=falseと書いたのに、if (flag)は真になりました。空でない文字列は、中身が "false" でも真として扱われるためです。VITE_PAGE_SIZE + 1は 21 ではなく "201" になりました。文字列の連結になるためです。
| 使いたい型 | 変換の書き方 |
|---|---|
| 真偽値 | import.meta.env.VITE_FLAG === "true" |
| 数値 | Number(import.meta.env.VITE_PAGE_SIZE) |
| 配列 | import.meta.env.VITE_LIST.split(",") |
一方、最初から用意されている DEV と PROD は真偽値なので、if (import.meta.env.DEV) とそのまま書けます。
環境変数を1か所で読み、足りないときに気づける形にする
ここまでの注意点(文字列の変換、足りない変数)をまとめて扱うために、環境変数を読む場所を1ファイルに集める形がおすすめです。
// src/env.ts
function required(key: keyof ImportMetaEnv): string {
const value = import.meta.env[key];
if (typeof value !== "string" || value === "") {
throw new Error(`環境変数 ${key} が設定されていません(.env と開発サーバーの起動場所を確認)`);
}
return value;
}
export const config = {
title: required("VITE_APP_TITLE"),
apiUrl: required("VITE_API_URL"),
pageSize: Number(required("VITE_PAGE_SIZE")),
newUi: required("VITE_FEATURE_NEW_UI") === "true",
};
// src/App.tsx
import { config } from "./env";
export default function App() {
return <pre id="out">{JSON.stringify(config, null, 2)}</pre>;
}
4つの変数がそろっているときの出力です。数値と真偽値に変換された状態で使えます。
{
"title": "備品管理",
"apiUrl": "https://api.example.com/v1",
"pageSize": 20,
"newUi": false
}
.env から VITE_API_URL の行を消して開き直すと、ブラウザのコンソールに次のエラーが出ました。
環境変数 VITE_API_URL が設定されていません(.env と開発サーバーの起動場所を確認)
undefined のまま処理が進んで別の場所でエラーになるよりも、起動した時点で「どの変数が足りないか」がわかるので、原因を探す時間を短くできます。前の章の型定義(strictImportMetaEnv つき)と組み合わせた状態で、npx tsc --noEmit がエラー0件で通ることも確かめています。
この形では import.meta.env を動的なキーで読むため、VITE_ で始まる変数がすべて成果物に入ります。VITE_ を付ける変数を「公開してよい値」に限っていれば、問題なく使えます。
よくある質問
Q. .env と .env.local はどう使い分けますか? A. .env にはチームで共有する既定値を書いてコミットし、.env.local には自分のPCだけで使う値を書いてコミット対象から外します。今回の実行結果では、同じ名前を両方に書くと .env.local の値が使われました。ただし .env.development などモード別のファイルに同じ名前があると、そちらが .env.local より優先されます。
Q. ビルドした後に環境変数だけ差し替えられますか?
A. 値はビルド時に JavaScript ファイルへ文字として埋め込まれるため、ビルド後に .env を変えても成果物は変わりません。環境ごとに値を変えるときは、npx vite build --mode staging のようにモードを指定して、環境ごとにビルドします。
Q. VITE_ を付けた値は外から見えますか? A. 見えます。今回 dist の中を grep したところ、値がそのままの文字で入っていました。URL や表示用の文言など、公開してよい値だけに VITE_ を付け、パスワードや秘密鍵はサーバー側で持つようにします。
Q. 開発では読めるのに、ビルド後だけ undefined になります。
A. まず import.meta.env.MODE を表示して、モードを確かめます。.env.development にだけ書いている、process.env で読んでいる(ビルド後はエラーにならず undefined になります)、CI に変数を登録していない、の3つが見るところです。
まとめ
- 変数名は VITE_ で始め、import.meta.env.VITE_名前 で読みます。 VITE_ のない変数はブラウザ側で undefined になり、成果物にも入りません。
- 優先順位は .env → .env.local → .env.[mode] → .env.[mode].local → 実行時にすでにある環境変数の順に高くなります(開発サーバーとビルドの両方で出力して確認)。
- undefined のときは、名前の接頭辞・置き場所・process.env・vite.config での参照・モードの順に確かめます。
- process.env は開発サーバーで ReferenceError、ビルド後は undefined になります。import.meta.env に統一します。
- vite.config の中では loadEnv を使います。 第3引数に空文字を渡すと、接頭辞なしの変数も読めます。
- 値はビルド時に dist へ埋め込まれます。 公開してよい値だけに VITE_ を付けます。
- 値はすべて文字列です。 "false" は真になるので
=== "true"で比べ、数値は Number で変換します。型定義には strictImportMetaEnv を足すと、打ち間違いを tsc が見つけてくれます。
参考・出典
- Vite 公式ドキュメント「Env Variables and Modes」 https://vite.dev/guide/env-and-mode
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Reactの環境変数.envの使い方|VITE_がundefinedの原因」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。