Save time by avoiding a rebuild of your React App in every CI stage
Tags: azure, devops, react, tutorial
Published: 2021-02-10 10:29:43

---
title: Save time by avoiding a rebuild of your React App in every CI stage
published: true
tags: #azure #devops #react #tutorial
canonical_url: https://darraghoriordan.com/2021/02/10/avoid-rebuild-react-app-ci-environments/
---

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 script that would work anywhere.

<!-- end excerpt -->

## 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.

```typescript
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'
  console.error(message)
  exit(1)
}

const providedFilePath = process.argv[2]

const envVarMappings = [
  {
    runTimeConfigProperty: 'appVariableOne',
    envVarName: 'REACT_APP_VARIABLE_ONE',
  },
  {
    runTimeConfigProperty: 'appVariableTwo',
    envVarName: 'REACT_APP_VARIABLE_TWO',
  },
]

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(
      x
    )}`

    console.error(message)
    exit(1)
  }

  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']= {`],
  mappedVariables,
  ['\r\n}']
)

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.

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

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.

```typescript
// 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.

```typescript
myThingThatDependsOnEnvironmentVariable(
  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.

```yaml
- 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
  env:
    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

```typescript
// 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

```typescript
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](https://storybook.js.org/docs/react/configure/story-rendering#adding-to-head).

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`.

```javascript
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`

```shell
runtime-config.js
```