When a React environment variable reads back as undefined, the cause is almost never a bug in React — it is the build tool refusing to expose a variable that lacks the right prefix. Create React App only forwards variables that start with REACT_APP_; Vite only forwards VITE_. Everything else is dropped on purpose, so a stray DB_PASSWORD in your .env cannot leak into a bundle that anyone can open in DevTools. The other half of the problem is timing: both tools read .env once at startup and inline the values at build time, so editing the file without restarting — or setting the variable in a hosting dashboard after the build — leaves the old value (or nothing) baked in. One more reality check for 2026: the React team deprecated Create React App on February 14, 2025, so if you are still debugging REACT_APP_ variables, the fix below is a stopgap and migrating to Vite is the real fix.
TL;DR
- Create React App exposes only
REACT_APP_-prefixed variables onprocess.env; Vite exposes onlyVITE_-prefixed variables onimport.meta.env. Anything else isundefinedby design. - Values are inlined at build time, not read at runtime. Editing
.envrequires a dev-server restart; changing a value in production requires a rebuild, not a redeploy. - The prefix is a publication marker, not a security boundary. Every exposed value ships as plain text in your JavaScript — never put a secret behind
REACT_APP_orVITE_. - The
.envfile must sit at the project root next topackage.json— not insrc/and not in a parent folder. - Create React App is in maintenance mode (deprecated February 2025). New React SPAs should use Vite; new apps that need routing and data fetching should use a framework.
Why is my React env variable undefined?
Nearly every report maps to one of six causes. The symptom is identical — a literal undefined where you expected a string — so work down the table by whether you are on Create React App or Vite.
| Symptom & cause | Create React App fix | Vite fix |
|---|---|---|
| Missing required prefix | Rename API_URL to REACT_APP_API_URL | Rename to VITE_API_URL |
| Edited .env while the server was running | Stop and restart npm start | Restart vite dev server |
| .env in the wrong directory (issue #3568) | Move it next to package.json, not src/ | Same, or set envDir in vite.config |
| Wrong file for the mode | npm start reads .env.development, not .env.production | vite build reads .env.production |
| Case typo / dynamic key access | Names are case-sensitive; use the literal process.env.REACT_APP_X | Use the literal import.meta.env.VITE_X, never [key] |
| Set in hosting dashboard after the build | Rebuild — values are inlined at build, not runtime | Rebuild and redeploy |
Issue facebook/create-react-app#3568 is the canonical version of the directory mistake: a CRA app nested inside an Express backend logged { NODE_ENV: "development", PUBLIC_URL: "" } and nothing else, because the .env sat at the monorepo root instead of the React app root.
Why do I have to restart the dev server after editing .env?
Both tools read .env exactly once, when the process starts, then statically substitute every reference in your source. Hot module replacement reloads your code, not your environment, so a value you change mid-session is invisible until the next boot. The CRA docs are explicit: "Changing any environment variables will require you to restart the development server if it is running." Vite behaves identically.
# Create React App — stop with Ctrl+C, then:
npm start
# Vite — stop with Ctrl+C, then:
npm run devREACT_APP_ vs VITE_: how do I read the variable in code?
The prefix and the access object differ, but the rule is the same: only prefixed names survive the build, and you must reference them as a literal dotted expression so static replacement can find them.
// Create React App — .env
REACT_APP_API_URL=https://api.example.com
API_URL=https://api.example.com // loaded, but never exposed
// component
console.log(process.env.REACT_APP_API_URL); // "https://api.example.com"
console.log(process.env.API_URL); // undefined — no REACT_APP_ prefix
// Vite — .env
VITE_API_URL=https://api.example.com
// component
console.log(import.meta.env.VITE_API_URL); // "https://api.example.com"
// Dynamic access breaks static replacement in BOTH tools:
const key = 'REACT_APP_API_URL';
console.log(process.env[key]); // undefined in the built bundleCreate React App also substitutes %REACT_APP_X% inside public/index.html, which is how analytics IDs reach a raw <script> tag. Vite uses %VITE_X% in index.html for the same purpose. The .env file priority is the standard dotenv cascade — more specific files win — and a variable already exported in your shell beats every file. The same first-wins behavior trips people up in plain dotenv.
Why is the variable undefined only in production?
Because a React SPA is static HTML/CSS/JS — there is no server reading process.env when a user loads the page. The value was frozen the moment npm run build ran. Three consequences follow:
- Adding the variable to Netlify, Vercel, or an S3/CloudFront pipeline after the build does nothing — the bundle is already written. Rebuild.
- Your CI runner must have the variable set in its environment at
buildtime, not just your laptop. A missing CI variable is the most common "works locally, undefined in prod" report. - Promoting one built artifact across staging and production freezes the value at the build environment's setting. If you need one image for many environments, see build-time vs runtime environment variables.
Can I read React env variables at runtime instead of build time?
Not through process.env or import.meta.env — those are compile-time substitutions and there is nothing left to "read" in the browser. If you genuinely need build-once / deploy-to-N-environments, inject the config at container start and read it from a global the bundle looks up at runtime:
// public/config.js — generated by an entrypoint script at container start,
// served as a static file and loaded before your bundle in index.html:
window.__ENV = { API_URL: "https://api.staging.example.com" };
// in app code — read the runtime global, not process.env:
const apiUrl = window.__ENV?.API_URL ?? "https://api.example.com";This is the standard pattern for Dockerized SPAs: a small entrypoint script writes config.js (or a fetched config.json) from the container's real environment, and index.html loads it before the app. The Next.js answer is different — server components read process.env per request — which is covered in the Next.js environment variables guide.
Is Create React App still maintained in 2026?
No. The React team deprecated Create React App on February 14, 2025, stating it "currently has no active maintainers." It "will continue working in maintenance mode," and a final version shipped to support React 19, but no new features are coming. The official guidance for new apps is a framework (Next.js, React Router, Expo); for a plain SPA, a build tool — Vite, Parcel, or Rsbuild. If you are debugging REACT_APP_ variables today, the migration is mechanical:
# 1. Drop react-scripts, add Vite + the React plugin
npm uninstall react-scripts
npm install -D vite @vitejs/plugin-react
# 2. Move public/index.html to the project root and add:
# <script type="module" src="/src/index.tsx"></script>
# 3. Rename every REACT_APP_ var to VITE_ and switch
# process.env.REACT_APP_X -> import.meta.env.VITE_XReal migrations report dev-server start dropping from roughly 15 seconds on Webpack to under 3 on Vite, with near-instant HMR — the practical reason teams move rather than wait for a maintainer who no longer exists.
When it is not a bug: a secret in a client env var is a leak
The most important "undefined" is the one you want. Both tools deliberately strip unprefixed variables so a private key never reaches the browser — the CRA docs put it plainly: "Do not store any secrets… anyone can view them by inspecting your app's files." So before you "fix" an undefined variable by adding a prefix, check what it holds:
- Database URLs, API secrets, signing keys — leave them unprefixed and undefined in the client. Proxy the request through a backend that holds the secret.
- Anything with billing attached — a publishable Stripe key (
pk_) is fine to expose; a secret key (sk_) prefixed by mistake is a live incident. - "It's only internal" — the bundle is public the moment it deploys. The prefix publishes, it does not protect.
If you have already shipped a prefixed secret, rotate it — it is in every cached build until you rebuild. The environment variable security guide covers the threat model, and the env validator flags syntax mistakes before they reach a build.
Frequently Asked Questions
Why is process.env.REACT_APP_MY_VAR undefined in Create React App?
Most often the variable lacks the REACT_APP_ prefix (CRA ignores everything except NODE_ENV), the dev server was not restarted after editing .env, the .env file is not in the project root next to package.json, or the name has a case typo. Variable names are case-sensitive and must be referenced as the literal process.env.REACT_APP_MY_VAR.
Do I need to restart the React dev server after changing .env?
Yes. Both Create React App and Vite read .env once at startup and inline the values, so a change is not picked up until you stop the server (Ctrl+C) and start it again. Hot reload refreshes your code, not your environment.
Why does my React env variable work locally but is undefined in production?
Values are embedded at build time, not read at runtime. The variable must be set in the environment that runs npm run build (your CI runner), not added to the hosting dashboard afterward. Setting it post-build has no effect — you must rebuild and redeploy.
How do I read a React env variable at runtime?
You cannot through process.env or import.meta.env — they are compile-time substitutions. For build-once, deploy-to-many setups, have a container entrypoint write a config.js that sets window.__ENV from the real environment, load it before your bundle, and read window.__ENV in code.
Are REACT_APP_ and VITE_ variables secure?
No. Every prefixed value is inlined as plain text into the shipped JavaScript and readable by anyone in DevTools. The prefix marks a variable as public; it does not protect it. Keep secrets unprefixed and on a server.
Should I still use Create React App in 2026?
No. The React team deprecated Create React App on February 14, 2025 and it has no active maintainers. It runs in maintenance mode only. Use Vite for new SPAs, or a framework like Next.js or React Router for apps that need routing and data fetching.
References
- Create React App — Adding Custom Environment Variables — the canonical reference for the
REACT_APP_prefix, build-time embedding, .env file priority, and the restart requirement - React — Sunsetting Create React App (Feb 14, 2025) — the official deprecation notice and migration recommendations
- facebook/create-react-app#3568 — REACT_APP_* undefined in development caused by the .env sitting in the wrong directory
- Vite — Env Variables and Modes — the
VITE_prefix,import.meta.env, and file priority - viject — one-shot migration tool from react-scripts (CRA) to Vite
Validate your .env files with the env validator, or compare prefixes across tools in the Vite and Next.js environment variable guides.