Wednesday, 2 June, 2021 UTC


Summary

Have you ever run into a problem in production/staging, when you just wanted to change the API URL in your React app in a quick and easy way?
Usually, to change the API URL you need to rebuild your application and redeploy it. If it's in a Docker container, you need to rebuild the whole image again to fix the issue, which can cause downtime. If it’s behind a CDN, you need to clear the cache too. Also, in most cases, you need to make/maintain two different builds for staging and production just because you are using different API URLs.
Of course, there have been solutions to solve these kinds of problems, but I found neither of them was self-explanatory and required some time to understand.
The resources out there are confusing, there are quite a lot of them and none was a package I could install and use easily. Many of them are Node.js servers which our client will query at the start on a specific URL (/config for example), require hard-coding the API URLs and changing them based on NODE_ENV, bash script injection (but that's not cool with someone developing on Windows without WSL), etc.
I wanted something that works well on any OS and also works the same in production.
We have come up with our solutions over the years here at RisingStack, but everyone had a different opinion about what is the best way to handle runtime environment variables in client apps. So I decided to give it a try with a package to unify this problem (at least for me:)).
I believe that my new package, runtime-env-cra solves this problem in a quick and easy way. You won't need to build different images anymore, because you want to change only an environment variable.
Cool, how should I use or migrate to runtime-env-cra?
Let's say you have a .env file in your root already with the following environment variables.
NODE_ENV=production
REACT_APP_API_URL=https://api.my-awesome-website.com
REACT_APP_MAIN_STYLE=dark
REACT_APP_WEBSITE_NAME=My awesome website
REACT_APP_DOMAIN=https://my-awesome-website.com
You are using these environment variables in your code as process.env.REACT_APP_API_URL now.
Let's configure the runtime-env-cra package, and see how our env usage will change in the code!
$ npm install runtime-env-cra
Modify your start script to the following in your package.json:
...
"scripts": {
"start": "NODE_ENV=development runtime-env-cra --config-name=./public/runtime-env.js && react-scripts start",
...
}
...
You can see the --config-name parameter for the script, which we use to describe where our config file should be after the start.
NOTE: You can change the name and location with the --config-name flag. If you want a different file name, feel free to change it, but in this article and examples, I’m going to use runtime-env.js. The config file in the provided folder will be injected during webpack build time.
If you are using another name than .env for your environment variables file, you can also provide that with the --env-file flag. By default --env-file flag uses ./.env.
Add the following to public/index.html inside the <head> tag:
<!-- Runtime environment variables -->
<script src="%PUBLIC_URL%/runtime-env.js"></script>
This runtime-env.js will look like this:
window.__RUNTIME_CONFIG__ = {"NODE_ENV":"development","API_URL":"https://my-awesome-api.com"};
During local development, we want to always use the .env file (or the one you provided with the --env-file flag), so that's why you need to provide NODE_ENV=development to the script.
If it gets development, it means you want to use the content of your .env. If you provide anything else than development or nothing for NODE_ENV, it will parse the variables from your session.
And as the last step, replace process.env to window.__RUNTIME_CONFIG__ in our application, and we are good to go!
What if I'm using TypeScript?
If you are using TypeScript, you must be wondering how it will auto-complete for you? All you need to do is to create src/types/globals.ts file, with the following (modify the __RUNTIME_CONFIG__ properties to match your environment):
export {};

declare global {
 interface Window {
   __RUNTIME_CONFIG__: {
     NODE_ENV: string;
     REACT_APP_API_URL: string;
     REACT_APP_MAIN_STYLE: string;
     REACT_APP_WEBSITE_NAME: string;
     REACT_APP_DOMAIN: string;
   };
 }
}
Add "include": ["src/types"] to your tsconfig.json:
{
"compilerOptions": { ... },
"include": ["src/types"]
}
Now you have TypeScript support also. :)
What about Docker, and running in production?
Here's an example of an alpine based Dockerfile with a multi-stage build, using just an Nginx to serve our client.
# -- BUILD --
FROM node:12.13.0-alpine as build

WORKDIR /usr/src/app

COPY package* ./
COPY . .

RUN npm install
RUN npm run build

# -- RELEASE --
FROM nginx:stable-alpine as release

COPY --from=build /usr/src/app/build /usr/share/nginx/html
# copy .env.example as .env to the release build
COPY --from=build /usr/src/app/.env.example /usr/share/nginx/html/.env
COPY --from=build /usr/src/app/nginx/default.conf /etc/nginx/conf.d/default.conf

RUN apk add --update nodejs
RUN apk add --update npm
RUN npm install -g [email protected]

WORKDIR /usr/share/nginx/html

EXPOSE 80

CMD ["/bin/sh", "-c", "runtime-env-cra && nginx -g \"daemon off;\""]
The key point here is to have a .env.example in your project, which represents your environment variable layout. The script will know what variable it will need to parse from the system. Inside the container, we can lean on that .env as a reference point.
Make sure you start the app with runtime-env-cra && nginx in the CMD section, this way the script can always parse the newly-added/modified environment variables to your container.
Examples
Here you can find more detailed and working examples on this topic (docker + docker-compose):
  • Create-react-app with typescript
  • Create-react-app without typescript
Link for the package on npm and Github:
  • npm
  • github
Hope you will find it useful!