# react-native-dotenv inlines env files into your bundle at build time

> A Babel plugin that replaces @env imports with literal values pulled from .env files while Metro builds, plus the precedence rules and the gaps that surprise teams.

**dotenvx/react-native-dotenv** — Load .env into React Native - with support for multiple .env files.

- Repository: https://github.com/dotenvx/react-native-dotenv
- Website: https://dotenvx.com/open-source
- Stars: 859 · Forks: 52
- Language: JavaScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/dotenvx-react-native-dotenv

## The plugin rewrites imports into literals rather than reading a runtime environment

react-native-dotenv is a Babel plugin, not a runtime library. The documentation describes it as a way to load.env into React Native with import statements, and the mechanism is visible in the plugin name and in the repository layout: index.js is the single published file, and the package manifest lists dotenv as its only runtime dependency. When Babel transforms your source, the plugin resolves the module name @env, reads the configured env files, and substitutes the actual string values into the compiled output. Nothing consults process.env at runtime, so a built bundle contains the literal value that was present on the machine that ran the build.

That design has a clear consequence. A value baked in at build time is as private as the JavaScript bundle that carries it, and the plugin prints a short confirmation line so you can see what it did: a text block reading injected env (2) from.env tells you two variables were pulled from the file. If your team expects secrets to be withheld from the artifact, this mechanism is the wrong starting point, because the plugin's job is precisely to remove the indirection. What it does give you is a named import surface, where a missing variable surfaces as an undefined value unless you set allowUndefined to false.

## Wiring the plugin into babel.config.js for a first @env import

The install is a single dev dependency, because the plugin runs on the build machine and has no reason to ship inside the app bundle.

```sh
npm install react-native-dotenv --save-dev
```

Next, register it in the Babel config. The plugin is referenced by its module specifier so Babel can resolve it from node_modules.

```js
// babel.config.js
module.exports = function (api) {
  api.cache(false)
  return {
    plugins: [
      ['module:react-native-dotenv']
    ]
  }
}
```

The api.cache(false) call matters for a practical reason: without it, Babel can reuse a cached configuration across runs, and changing env files does not necessarily change the transform. Create the env file in the project root next.

```ini
# .env
HELLO="Dotenv"
API_URL=https://api.example.org
```

Then import the names you need, exactly as they are spelled in the file.

```js
// App.js
import { HELLO, API_URL } from '@env'

console.log(`Hello ${HELLO}`)
fetch(`${API_URL}/users`)
```

The names must match the file keys exactly. The plugin does not generate a type or a stub for a variable you never declared.

## The dotenv CLI selects a file for Metro but leaves the plugin reading its own

The advanced section of the documentation pairs the plugin with dotenv v18 and its run command, and the precedence here is the part that catches people. The CLI needs to be installed on its own so the package manager exposes its binary to your project scripts.

```sh
npm install --save-dev dotenv@^18.0.1
```

A script then selects the file that Metro starts with.

```json
{
  "scripts": {
    "start:staging": "dotenv run -f .env.staging -- react-native start --reset-cache"
  }
}
```

Here is the trap. The -f flag selects the CLI's file, not the plugin's path or APP_ENV setting. The plugin still loads its own configured files, but the values the CLI injected into the process environment take priority, and the documentation spells this out: plain dotenv run loads .env into the process environment, so those values win over the plugin's .env.local values. Existing shell and CI values also win unless you pass --override to dotenv run. Two separate mechanisms are loading the same key space, and the environment wins.

Restarting matters too. The CLI sets the process environment when the script starts, so you must stop Metro and rerun the script after changing a CLI loaded value. The reset-cache flag in the example clears Metro's transform cache, which is what lets the new literals reach the bundle.

## envName selects a mode file and cannot be set to development or production

By default the plugin reads the APP_ENV environment variable to decide which mode file applies, and that variable is separate from NODE_ENV; the envName option lets you rename it. The documented reason is that Metro can overwrite the test environment even if you specify a config, so the plugin offers a dedicated override.

```json
// package.json
{
  "scripts": {
    "start:staging": "APP_ENV=staging npx react-native start"
  }
}
```

With that script, the.env.staging file is used. You can rename the selector through the plugin options.

```json
{
  "plugins": [
    ["module:react-native-dotenv", {
      "envName": "MY_ENV"
    }]
  ]
}
```

The restriction worth knowing before you pick a scheme: if you use APP_ENV or a custom envName, you cannot use development or production as values, and the documentation advises against having a.env.development or.env.production file in that case. The reason given is a Babel and Node behaviour the author says he has little control over, and he points to Gatsby as a platform with a similar override. The suggested alternative is to drop APP_ENV entirely and rely on the built in NODE_ENV=development or NODE_ENV=production, or to distinguish debug from release modes. If you do keep APP_ENV, note the caveat that it compiles for the start command only, so you have to run react-native start every time you change values.

## moduleName, allowlist and blocklist decide which names can be imported at all

Three options control the surface area of the import, and they are worth setting deliberately rather than leaving at defaults. The moduleName option renames the virtual module used in import statements, and it defaults to @env.

```json
{
  "plugins": [
    ["module:react-native-dotenv", {
      "moduleName": "react-native-dotenv"
    }]
  ]
}
```

The documentation recommends that name for TypeScript or Next.js setups, where a bare @env specifier does not resolve for the type checker. The allowlist option restricts imports to a named set of variables, and the blocklist option prevents specific names from being imported.

```json
{
  "plugins": [
    ["module:react-native-dotenv", {
      "allowlist": [
        "HELLO",
        "API_URL"
      ]
    }]
  ]
}
```

Together these give you a compile time statement of intent about which configuration keys exist. A name that is not allowed cannot be imported, so a typo in an import fails during the build rather than at runtime. The practical value of the blocklist is that a key such as a personal token living in the same .env file cannot leak into the app through an accidental import, even if it is present in the file.

## safe mode narrows imports to keys the files declare, and leaves process.env mostly empty

The safe option defaults to false, and flipping it changes what reaches your imports in a way that is easy to misjudge. With safe enabled, only variables defined in your .env files are allowed as imports. Host environment variables can still override values for those same keys at build time, but keys that exist only in the environment are not imported at all.

```json
{
  "plugins": [
    ["module:react-native-dotenv", {
      "safe": true
    }]
  ]
}
```

The documentation goes further and recommends setting allowUndefined to false alongside safe mode, so a key that is expected but missing becomes a hard failure.

The related limitation applies to process.env as well. The documentation states that process.env.X is only inlined for keys present in the plugin's files, plus the special names NODE_ENV, BABEL_ENV and envName. So a bare process.env.API_URL written directly in application code will not pick up an arbitrary shell variable the way it would in a Node server process. This is the main mental adjustment for developers moving from a web backend mindset: in a React Native app, configuration reaches code through the @env import or through those three special names, not through the ambient environment.

One more sharp edge sits at the intersection of safe mode and the CLI. With the default safe: false, keys loaded only by the CLI work through @env imports. With safe: true, those keys must also appear in files the plugin reads, so a value that exists purely in the process environment disappears from your imports even though the CLI set it.

## Expo projects need a cleared Metro cache to see the values

Expo is covered as its own case, because the preset differs and the cache behaves differently. The Babel config keeps the same plugin entry but adds the Expo preset.

```js
// babel.config.js
module.exports = function (api) {
  api.cache(false)
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      ['module:react-native-dotenv']
    ]
  }
}
```

The import shape is the same as in a plain project, and the documentation pairs it with a React Native Text component.

```js
// app/index.tsx
import { HELLO } from '@env'
import { Text } from 'react-native'

export default function HomeScreen() {
  return <Text>Hello {HELLO}</Text>
}
```

The final instruction is the part that saves an afternoon. Start Expo with a clean Metro cache using npx expo start --clear. Without that, a stale transform can keep serving the previous literals after you have edited the .env file, and the symptom looks like the plugin ignoring your file. The same underlying reason explains the reset-cache flag in the dotenv run script and the api.cache(false) setting in the Babel config. In every case the plugin is doing its job, and the caching layer above it is holding on to an older build.

## The manifest version and the release list have drifted apart

The package manifest declares version 5.0.0, while the published releases visible for this project stop at v3.4.10, dated 2024-02-11, with v3.4.9 from 2023-06-18 and v3.4.2 from 2022-11-12 in between. The repository is not archived, and the last push was on 2026-10-19, so the code is being worked on even though the tag list does not show a matching release. Treat the two numbers as separate facts: the version your lockfile resolves is what determines behaviour, and the release names alone will not tell you which behaviour you have.

The upgrade surface is small because the published artifact is one file. The files array in the manifest lists index.js only, and the only runtime dependency is dotenv at ^18.0.1. The devDependencies are the test and lint tooling, with jest configured to collect coverage from index.js and to ignore the fixtures directory. The project uses standard for linting and jest for tests, and the repository root contains the usual CHANGELOG.md, CONTRIBUTING.md, SECURITY.md and LICENSE files.

Licensing is MIT, according to both the manifest and the repository root. That is permissive enough for commercial use in most projects, but note what the license does not change: the values you inline are still plain strings inside your shipped bundle, so the license of the plugin is not a substitute for deciding what belongs in a mobile artifact.

## Conclusion

Adopt react-native-dotenv when your app imports configuration as named constants and you want those values frozen into the bundle at build time. Skip it when secrets must stay out of the artifact, since the whole design is literal inlining, and note that allowlist, blocklist and safe mode all operate on what the plugin can read from files, not on what the runtime process holds. Before committing, verify which .env file your build actually selects, check that your CI environment does not silently override a value through dotenv run, and confirm that the version you install matches the package.json you expect, since the published releases list stops at v3.4.10 while the manifest declares 5.0.0.

## FAQ

### How do I load a .env file in a React Native app with react-native-dotenv?

Install the plugin as a dev dependency, add 'module:react-native-dotenv' to the plugins array in babel.config.js, put your variables in a .env file in the project root, then import them from '@env'. The plugin prints a confirmation line such as injected env (2) from .env showing how many variables it inlined.

### Why does react-native-dotenv keep using my old environment values after I edit the .env file?

Metro caches transforms, so a stale bundle can keep the previous literals. Restart Metro with a cleared cache, and for Expo use npx expo start --clear. If you inject values with the dotenv CLI, stop Metro and rerun the script, because its process environment is set at startup.

### Can react-native-dotenv import variables that only exist in my shell or CI environment?

With the default safe: false, yes, keys that exist only in the environment can be imported. With safe: true, only keys defined in your .env files are allowed, although host values can still override those same keys at build time. process.env.X is inlined only for keys in the plugin's files, plus NODE_ENV, BABEL_ENV and envName.

### How do I pick between .env, .env.staging and APP_ENV in react-native-dotenv?

The envName option, defaulting to APP_ENV, selects the mode file and works separately from NODE_ENV. If you use APP_ENV or a custom envName, you cannot use development or production as values and should avoid .env.development and .env.production files. Use the built in NODE_ENV values or debug versus release modes instead.

### What license does react-native-dotenv use?

The project is released under the MIT license, as shown in the package manifest and the LICENSE file in the repository root. That license covers the plugin code, not the values you inline into your application bundle.

## Sources

- [dotenvx/react-native-dotenv on GitHub](https://github.com/dotenvx/react-native-dotenv)
- [License: MIT](https://github.com/dotenvx/react-native-dotenv/blob/main/LICENSE)
- [Project website](https://dotenvx.com/open-source)
- [README](https://github.com/dotenvx/react-native-dotenv/blob/main/README.md)
- [Releases](https://github.com/dotenvx/react-native-dotenv/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dotenvx-react-native-dotenv
