Avoid rebuild of React App in every CI stage

If you have a react app you can use env vars like REACT_APP_MY_ENV_VAR in your application and React will automatically pull them in to your app when you build the production application.

This is very useful but if you have variables that change for each environment and your application build takes a long time, you might want to avoid building unnecessarily in CI. For example you might have a QA environment and a Staging environment that have different configuration.

We type-check our code on each build and that was taking 5 minutes+ to build each environment so we had to make it faster. We changed our app from using REACT_APP env vars to using a configuration file that we could quickly write to using CI.

Our CI system is Azure DevOops so the CI scripts here are specifically for Azure DevOps but they apply to most CI systems with small changes.

The real work happens in a Node.js script that would work anywhere.

Add the script to write env vars to file

Here we have a script that will take all the environment variables that we have mapped in the mapping configuration and will write them to a JavaScript file. The we will attach our configuration to the window when the script is run.

This script runs in a couple of seconds in comparison to 5-10 minutes for a build with type checking.

const fs = require('fs')
const { exit } = require('process')

if (!process.argv[2]) {
  const message =
    'You must provide a file path to write the generated file to as an argument to this script'

const providedFilePath = process.argv[2]

const envVarMappings = [
    runTimeConfigProperty: 'appVariableOne',
    runTimeConfigProperty: 'appVariableTwo',

const mappedVariables = envVarMappings.map((x) => {
  if (process.env[x.envVarName] === undefined) {
    const message = `The webapp property configured does not have an environment variable set. The environment variable must be present : ${JSON.stringify(


  return `\r\n${x.runTimeConfigProperty}: '${process.env[x.envVarName]}',`

// write out the lines to a script that attaches the variables to the window
const runtimeConfigFileAsLines = [].concat(
  [`window['runtimeConfig']= {`],

fs.writeFileSync(providedFilePath, runtimeConfigFileAsLines.join(' '))

Modify your app to use the config file

Add a script tag in the head section of the index.html in your React application. We use the %PUBLIC_URL% variable here which will be replaced by react for us.

  <script src="%PUBLIC_URL%/runtime-config.js"></script>

This tells React to load our config file which will set the configuration object on the window object.

Next wrap the configuration object in an interface if using typescript. You can skip some of this if using JavaScript.

// These values will be sent to the client so do not add
// secrets here.
export interface RuntimeConfig {
  appVariableOne: string
  appVariableTwo: string

export const runtimeConfig: RuntimeConfig = window.runtimeConfig
export default runtimeConfig

Now you can access the configuration object anywhere that you used to use a REACT_APP_ variable before.

In our variable access statement we try to use the configuration file but if it doesn’t exist then we will look for the old environment variable. This works in a backwards compatible way with environment variables.

  runtimeConfig.appVariableOne || process.env.REACT_APP_VARIABLE_ONE

Add a CI step to generate the environment specific configuration file

We add a CI step to run the configuration file generator in our infrastructure folder.

We have to chmod it runnable first.

- script: |
    chmod +x ./infrastructure/pipeline/write-webapp-runtime-config.js
    node ./infrastructure/pipeline/write-webapp-runtime-config.js ./packages/react-client/build/runtime-config.js
    REACT_APP_VARIABLE_ONE: $(appVariableOne)
    REACT_APP_VARIABLE_TWO: $(appVariableTwo)
  displayName: 'Write react app runtime variables'

Configure Jest

If you have any tests that depend on the configuration then you need to tell Jest to load the file before running tests.

To do this you add a preRun file (unless you already have one) and add that to the “setup” property in the jest configuration

// add this to a file called "jest.prerunsetup.js" or similar
window.runtimeConfig = window.runtimeConfig || {}

now in your jest.config.js add a reference to that setup file

module.exports = {
  setupFiles: ['./jest.prerunsetup.js'],

Configure other tools

Any tool that uses React components will need to have the configuration injected. The Jest way is mentioned above. Each too will have it’s own injection method. For example if you use react storybook you will need to add the script to the header using the storybook method described here.

Add a file .storybook/preview-head.html and pop the header script from above in there.

Add a local configuration file (if you like)

This should just go in your <<app>>/private folder if you’re using create-react-app.

window['backrRuntimeConfig'] = {
  appVariableOne: 'value1',
  appVariableTwo: 'value2',

You can put your development settings in here.

Git ignore the local config file

Just like a .env file you will want to .gitignore your local copy of the configuration.

Add to .gitignore

Darragh ORiordan

Hi! I'm Darragh ORiordan.

I live and work in Sydney, Australia building and supporting happy teams that create high quality software for the web.

I also make tools for busy developers! Do you have a new M1 Mac to setup? Have you ever spent a week getting your dev environment just right?

My Universal DevShell tooling will save you 30+ hours of configuring your Windows or Mac dev environment with all the best, modern shell and dev tools.

Get DevShell here: ✨ https://usemiller.dev/dev-shell

Read more articles like this one...

List of article summaries


Migrating a Create React App (CRA) application to Vite

I had an existing app that was scaffolded using create react app (CRA) and extended with craco. CRA didn’t support the tooling I needed so I had to look for an alternative. I found Vite.

There are some incredible improvements in Vite over CRA, including PostCSS 8.0 support so I decided to migrate my production application.

I’ll explain some of the benefits of Vite and describe the steps you need to take to upgrade your application.

Update October 2022: I changed @vitejs/plugin-react-refresh to @vitejs/plugin-react. The former is deprecated now.


Fix React Router navlink-exact when activeClass is not working

If you’re adding navlinks with react router because you want to set the active class you might find that it doesn’t work. These are the steps I had to take to make this work.


Setting and debugging Azure Devops yaml env vars

I received an interesting question yesterday that highlighted how some parts of environment variables in Azure devops are not clear. Here are some things to check if your environment variables aren’t working in azure pipelines.