Software needs configuration. We can all agree on that. Configuration needs to be separate from the code. We can (hopefully) all agree on that.

A common way of configuring software is by using environment variables. In this article, we’ll learn how to use them in React. Specifically, in React Single Page Apps. More specifically, in React SPAs that are statically served for different environments such as development, staging, and production.

Ready to dive in?

What are Environment Variables Anyway?

Let’s start by calling them “env vars” from now on. My keyboard will thank me. Wikipedia defines them as:

An environment variable is a user-definable value that can affect the way running processes will behave on a computer. Environment variables are part of the environment in which a process runs.

Wait, what? A process? This is React SPA! I thought we were statically serving a mix of HTML, JS, and CSS to a web browser. That’s true. So for the sake of our discussion, let’s bluntly ignore Wikipedia and treat env vars as a form of configuration.

Here are some examples of things that we configure here at Renbizz:

  • Backend URLs (every developer has their own environment, and each environment naturally has a different URL).
  • 3rd-party services’ URLs and/or client IDs (authentication, user analytics, feature flags, tracing, etc.) - we keep our environments separated in these services as well.

Why Should I Use Environment Variables in React?

First, they allow us to keep our codebase code-based. Separating our application code from its configuration is one of the best best-practices there are.

Second, every software project’s transition from “toy-mode” into “actual-product-people-use” mode starts with separation of environments. Usually this starts with a “production” environment (that’s separated from the development environment), and may continue to a staging environment, maybe a test automation environment, a demo environment, a dev environment for each developer in the team, and the list goes on.

All of these different environments run (more or less) the same code base, but differ in their configuration. You don’t want your production user analytics to include dev or test events, for example. You want to be able to know for sure that your frontend reaches the correct backend endpoint (that runs the code you intend it to run).

This separation is a necessity in every mature or mature-aspiring software.

Never Store Secrets in React Environment Variables!

If you read this article - I hope you already know this - and still: NEVER STORE SECRETS IN A STATIC REACT WEBSITE!

Every visitor can see your code and your network traffic. When serving static websites, your env vars become part of your served code. The last thing you want is people messin’ where they shouldn’t been messin'.

One of these days these boots are gonna walk all over you

*** If you still think you need to store secrets in your react environment variables, you probably need to rethink your architecture. Hit me up - I’ll be happy to assist.

So How Do I Use These Variables?

I’ll start by briefly describing how most tutorials online say you should. I’ll spice it up with why I think it is wrong. Then wrap it with how we use them here at Renbizz. Let’s go.

There’s usually a .env file involved. This file is a simple list of KEY=value entries that will be made available. In your code, this may look something like:

await fetch(process.env.BACKEND_ENDPOINT_URL, {...})

To avoid unintentionally exposing secrets and unwanted variables used by your operating system, these variables will be prefixed according to the bundling mechanism you use (VITE_BACKEND_ENDPOINT_URL, REACT_APP_BACKEND_ENDPOINT_URL, etc).

Sometimes a single file will hold your different environment’s variables (PROD_BACKEND_ENDPOINT_URL, STAGING_BACKEND_ENDPOINT_URL, …). Sometimes you’ll have completely separate .env files for your different environments (.env.production, .env.staging, …).

How are they getting “injected” into the code? Both Vite and CRA (If that’s still a thing) use the dotenv package under the hood to expose the contents of your .env file(s) to your code, during development and after bundling.

This all works great on local development machines, but what do we do on CI/CD? Naturally .env files are git-ignored and won’t be commited to git. That means that our CI/CD pipeline would have to get these configuration values elsewhere.

On top of that, my personal preference is to build once for staging and production - so anything that bundles environment variables into the bundle won’t work out of the box.

Requirements For a Robust Solution

  • Store the variables remotely.
  • Use the remote variables when running locally in dev mode.
  • Use the remote variables when deployed to production.
  • Be able to change the variables on an already bundled app.

Suggested Solution

  • Autogenerate a env.js file that would set the vars on the global scope (so they can be used anywhere).
  • Import it in index.html.
  • Serve it next to your html and js files.

React SPA Environment Variables, Complete Step-By-Step Guide (The Renbizz Way)

  1. Store the configuration remotely. Somewhere that’s accessible for both your local dev machine and your remote CI/CD runners. We use AWS Systems Manager Parameter Store. This works great for us because our backend runs on AWS and we can use these configuration values (and others) directly from it.

  2. Edit vite.config.ts to tell Vite to treat your soon-to-be-created env.js file as an external module:

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      external: './env.js',
    },
  },
})
  1. Edit your index.html file to source this script:
<!doctype html>
  <head>
    <script type="module" src="./env.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
  1. Write a configure script (can be bash, node, python, or whatever you feel comfortable with). This script will read the env vars from the remote source and dump them in your env.js file:
import {writeFile} from  'fs/promises'
import {EOL} from  'os'

const envVars = { // replace this with actual code
  VITE_BACKEND_URL: await getValueFromSomeRemote('backend-url')
}
const fileLines  = ['// This file is auto-generated by configure.mjs']
fileLines.push('globalThis.env = globalThis.env || {}')
fileLines.push(
  ...Object.entries(envVars).map(([key, value]) => `globalThis.env.${key}='${value}'`)
)
for (const filePath of ['public/env.js', 'dist/env.js']) {
  await writeFile(filePath, fileLines.join(EOL))
}

This script will generate the following file:

// This file is auto-generated by configure.mjs
globalThis.env = globalThis.env || {}
globalThis.env.VITE_BACKEND_URL='https://backend.example.com'

You may have noticed we saved this file into 2 locations. Each one serves its own purpose:

  • public/env.js: When running locally, Vite will serve the script from this directory.
  • dist/env.js: After we bulid and bundled our code, we need to place our file in this directory for it to be served alongside our html. Sidenote: Vite will copy the file from the public dir to the dist dir when we bundle.

Make sure both are git-ignored!

  1. Edit package.json to run your configuration script before you run in dev mode and before you deploy:
{
  ...
  "scripts": {
    "configure": "node configure.mjs",
    "predev": "yarn configure", // Configure before running locally in dev mode
    "dev": "vite",
    "build": "vite build",
    "predeploy": "yarn build",
    "predeploy-only": "yarn configure", // Configure before deploying
    "deploy-only": "node deploy.mjs",
    "deploy": "yarn deploy-only"
  }
}
  1. Bonus points: React Environment Variables <3 Typescript! We declare the type of all of our variables in a global.d.ts declerations file, and include it in the include section of our tsconfig.json:
export {}

declare  global {
  // eslint-disable-next-line no-var
  var  env: {[key:  string]:  string}
}

Takeaways

I hope you found this guide useful. I hope even more that that you will actually use it!

When you do - please let me know. I’d love to see this method out in the wild.