Revelry

AI-Driven Custom Software Development

React

The One Frustrating Assumption That Breaks React Environment Variable Setup (And How to Fix It)

Earlier this year, I became one of the unlucky few, that special class of coder for whom the bell tolls. I followed advice from the internet, and it didn’t work for me. I needed to get a local react app setup for deployment. Ring-a-ding-ding, that’s when I heard from my old friend; environment variables.

 If you Google “react env var setup” the results will most often show “four easy steps” to setting up react environment variables. However, these four easy steps are based on an underlying assumption that the common advice overlooks about the  env var setup, and assumptions will shoot to kill.

These setup steps silently assume that you have generated your project with create-react-app or have react-scripts installed in your project. In this article, I’m going to explain a little about environment variables in React, so you can avoid those assumptions. I’ll go over the suggested steps to setting them up, why those steps work when they do, and how to set up environment variables for a project that uses React and Webpack, but does not use react-scripts.

Environment variables, or as they are known on the street, env vars, are values set externally for a specific deployment environment. Unlike variables declared within your application code, env vars are referenced by your code but defined as part of the environment setup. These are used to develop locally, but deploy to production, while pointing to separate resources, accounts, APIs, etc. As an example, you may write songs on an acoustic guitar in your bedroom, but when you play in front of an audience, you may play the same song on an electric guitar and amp. It’s the same song, played the same way, but the environment and instrumentation are different.

Client-side frameworks like React may use an env var for hitting an auth client, setting a url, adding feature flags and anything else that is environment specific, but not secret. Secrets should never go on your client, because then they are no longer secret! Even though they can’t truly hide information, environment variables are extremely useful in software development across multiple environments, and setting them up can seem trivially easy.

A web search on how to set up env vars in a React project will often list just a few steps:

  1. Create a .env file to house your variables
  2. Declare your environment variables. Our first clue: (there is often a note about how it is crucial to prefix your env variables with REACT_APP_
REACT_APP_BASE_URL="http://localhost:8080"

3. Access your environment variables using process.env. This is clue number 2. process.env doesn’t typically exist in the browser. It’s part of Node.

console.log(process.env.REACT_APP_BASE_URL)

4. Restart your development server.

There are youtubes about this, blog posts, the stack overfloweth.  So you may be surprised if you have a react application, have a .env file, and have variables prefixed with REACT_APP_ only to have your process.env end up empty. You wonder helplessly what could have gone wrong as you shrivel into a microchip and scream “I’m not pwned, I’m now pwned!”.

What’s gone wrong is an assumption. The advice from those youtubes, blogs, and stack overflow posts assumes that you have created your react app using create-react-app. This is a reasonable enough assumption. There was a period where create-react-app was the standard for generating a react project. We are however, no longer in that period, and there are multiple ways to generate a react project, and they don’t all work the same way. 

My project was generated using yo office. My project had a good webpack config, all the react stuff was… reacting, but my .env vars wouldn’t load. The approach I finally took was installing dotenv and including it in my webpack.

npm i -S dotenv
// webpack.config.js
require("dotenv").config();

 Then using webpacks DefinePlugin, to define custom top level variables, based off of my .env and a config.json.

// webpack.config.js
new webpack.DefinePlugin(
                Object.fromEntries(Object.entries(config).map(([key]) => [key, JSON.stringify(process.env[key])]))

 I would load the config json, loop over its keys, look in the process.env for the matching key, and load the value into the DefinePlugin. To reference my variables, I then had to use the declare keyword to let typescript know the variables would exist at runtime.

declare let PLATFORM_API_URL: string;

Then I could reference my .env variables. I was feeling like the internet was giving me the High Hat. It worked, but the annoyance was grinding my gears like something stuck in the back of my craw. I eventually realized that all the answers I was seeing for setting this up were not pointing to react docs, but were pointing to create-react-app docs.

So I opened up the Github for create-react-app, and looked at whether or not they were using the DefinePlugin like I was. This stack overflow pointed me in the right direction and I had a new lead; react-scripts. I found this line in the react-scripts package, where a variable name env is initialized to the result of a function call imported from another module.

 const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));

That led me to the env file, where I saw some familiar keywords like process.env, REACT_APP, and dotenv. Looking at what the getClientEnvironment function does, it becomes clear. It iterates over the keys in process.env, filters for keys that match a delimiter, which in this case is REACT_APP, and then adds that to a process.env object that it exposes.

This allows React apps to expose a custom fake process.env. Taking this approach for inspiration, I made an allowlist of env variables, loaded process.env, filtered for allowlist matches, and exposed my own process.env to my application. I was able to remove the declares,  and reference my variables directly from process.env. I also got rid of the config.json file I was using to list the keys, because I realized that the variables I wanted to reference would be on the process.env during the webpack bundling process.

If you are making a react app without create-react-app, or vite, or react-scripts, and you want to use env variables, you can inject them into webpack using the define plugin, and draw right from the process.env!

const webpack = require("webpack");
require("dotenv").config();

const allowlist = [
  "API_URL",
  "AUTH_DOMAIN",
  "AUTH_CLIENT_ID",
  "AUTH_AUDIENCE",
  "ENABLE_FEATURE",
];

module.exports = async (env, opt) => {
  const config = {
// rest of your config...
    plugins: [
      new webpack.DefinePlugin({
        "process.env": Object.fromEntries(
          Object.entries(process.env)
            .filter(([key]) => allowlist.includes(key))
            .map(([key, value]) => [key, JSON.stringify(value)])
        ),
      }),
    ].filter(Boolean),
// rest of your config...
  };

  return config;
};

The case started simple enough, but the code started to look like my mom’s spaghetti, sauce and all. Sometimes in order to keep your code clean in this world, you have to be ready to get dirty and investigate. You have to follow the clues wherever they lead; from the seedy alleys of stack overflow, to the cold cruel underworld of open source github repositories. But now my investigation is complete. Now I feel like a million bucks, and my code, well, ended up with a one way ticket to github.