react-native-dotenv

A Babel plugin that loads .env files into React Native and Expo apps, inlining environment variables at build time via @env imports.

Tool
npm
v4.1.1
859stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
79/100Good
Development Activity96
Maintenance72
Community48
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
69/100Good
Architecture72
Code Quality75
Innovation58
Learning Curve70

react-native-dotenv is a Babel plugin maintained by the dotenvx team that lets React Native and Expo projects import environment variables directly from .env files using import { HELLO } from '@env'. Because React Native has no Node.js process.env at runtime, the plugin parses .env (plus mode-specific and .local overrides, dotenv-flow style) during the Babel transform pass and replaces each imported binding with its literal value before the bundle is built.

It supports multi-environment configuration (.env, .env.<mode>, .env.local, .env.<mode>.local) selected via APP_ENV/BABEL_ENV/NODE_ENV, along with allowlist/blocklist restrictions, a safe mode that only allows declared keys, and a configurable moduleName for compatibility with tools like Next.js that already claim @env. It also selectively inlines matching process.env.X references so existing code doesn’t have to switch to the @env import style.

What You Get

  • Compile-time @env imports that inline .env values as literals in the bundle
  • Multi-environment file resolution (.env, .env.<mode>, .env.local, .env.<mode>.local) based on dotenv-flow conventions
  • allowlist/blocklist options to restrict which keys are importable
  • safe mode that only allows declared .env keys, with an allowUndefined toggle for missing values
  • Configurable moduleName so it doesn’t collide with meta-frameworks like Next.js that already use @env
  • Selective process.env.X inlining that mirrors @env imports without requiring code changes

Common Use Cases

  • Storing API base URLs and feature flags for a React Native or Expo app without shipping a runtime config file
  • Running the same codebase against staging vs production environments via APP_ENV/NODE_ENV-selected .env.<mode> files
  • Making non-secret build config (analytics keys, environment name) available to app code without a native module
  • Migrating a Next.js + React Native codebase where @env is already claimed, via the configurable moduleName option

Under The Hood

Architecture The whole plugin lives in one flat index.js exporting a Babel plugin factory (api, options) => {...}. On each invocation it merges options with defaults, derives the active mode name from options.envName (default APP_ENV) falling back to BABEL_ENV/NODE_ENV, then reads up to four candidate dotenv files (.env, .env.local, .env.<mode>, .env.<mode>.local) via a small parseDotenvFile() helper wrapping fs.readFileSync + dotenv.parse, merging them in that precedence order into a single env object and a processEnvInlineKeys set. It wires Babel/Metro cache invalidation through api.cache.using(mtime(...)) and api.addExternalDependency for each candidate file, then returns a visitor with two handlers: ImportDeclaration rewrites import { X } from '@env' into inlined literals (throwing Babel code-frame errors for default/wildcard imports or allowlist/blocklist violations), and MemberExpression rewrites process.env.X only for keys present in the merged env or a small mode-exception set. There is no internal layering by design — it is a single-purpose transform, and the one place a change would ripple everywhere is the merge order feeding the shared env object and processEnvInlineKeys set that both visitor handlers read from.

Tech Stack Plain CommonJS JavaScript with no TypeScript and no build step — the published package ships index.js directly (files: ["index.js"] in package.json). Its only runtime dependency is dotenv (^17.4.2) for .env parsing; it consumes the Babel plugin API (api.types, api.cache, api.addExternalDependency) supplied by whichever Babel/Metro host loads it, with @babel/core present only as a devDependency for tests. Linting uses standard (zero-config JS Standard Style) rather than an ESLint config, and tests run under Jest 30. It has no database, server, or bundler of its own — it is consumed as a Babel plugin inside a React Native/Expo/Metro or Next.js build pipeline.

Code Quality Testing is thorough for the surface area involved: tests/index.test.js exercises error paths (undefined variable, default import, wildcard import), successful .env loading, verbose/quiet logging, a log-once-per-process guard, and multi-env precedence, backed by roughly 30 fixture directories under tests/fixtures/ covering allowlist, blocklist, safe mode, process.env propagation, and more. Jest is configured with collectCoverage scoped to index.js and coverageProvider: v8. CI (GitHub Actions) runs npm audit, standard, and the full test suite with cache disabled across Node 22/24/26 on every push and PR. Error handling favors explicit, user-facing failures — Babel’s buildCodeFrameError for config violations — while missing .env files are caught and treated as absent rather than crashing. There is no static type system (no TypeScript, no JSDoc types), which is the main quality gap for a codebase otherwise well covered by tests and CI.

What Makes It Unique The design choices are incremental refinements on the well-established “inline env vars at build time” pattern (the same idea behind Next.js’s built-in env inlining or webpack’s DefinePlugin), tailored specifically to React Native/Metro’s quirks rather than introducing a new technique: process.env.X inlining is deliberately scoped only to keys that appear in .env files (plus a small mode-exception allowlist) so unrelated build-tool environment variables never leak into the bundle; dotenv-flow-style multi-file precedence lets teams layer base, mode-specific, and local overrides; a process-level dedup set stops Babel’s per-file re-instantiation (common when api.cache(false) is set) from spamming stderr with repeated “injected env” messages; and cache invalidation is wired through Babel’s own api.cache.using/addExternalDependency hooks so Metro picks up .env changes without always requiring a manual cache reset.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search