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
:
Hope you will find it useful!