Avoid rebuild of React App in every CI stage

Photo by Sergi Kabrera on Unsplash

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";
  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.

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

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

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.

- 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

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

runtime-config.js