20

I've built a reactjs site and trying to get it to be deployable.

right now all configuration is done via a config.js which is imported into all modules.

But when I build the app, this gets compiled into the deployment js and isn't configurable.

I want to have a separate file which a sys admin can configure various settings specific to their enviroment, such as the API end points (the app may not be running on the same server as the backend, and there will not be any access to DNS).

Is there a way to do this in react? I do not wish to use 3rd party libraries for this either.

Space Bear
  • 755
  • 1
  • 6
  • 18
  • Use environment variables. – Anurag Awasthi Aug 02 '18 at 12:53
  • 7
    No. These have to be configurable at run time. This application will be deployed to customer sites all with different network configuration (likely air gapped) hitting an api on their local network. Their sys admins need to be able to just drop in edit a config file, then be off to the races. – Space Bear Aug 02 '18 at 12:56
  • Can you please help me I got error in the public/index.html file [error] window.RenderApp is not a function – Ravindra Nakrani Jul 12 '22 at 15:15
  • I put together a full example for the very same use case [here](https://stackoverflow.com/questions/52103155/reading-an-environment-variable-in-react-which-was-set-by-docker/74701083#74701083). – jschnasse Dec 07 '22 at 14:04

7 Answers7

24

Not sure about the linux approach, but I am using create-react-app (cra), Docker & kubernetes a lot and initially struggled with similar problem. I was then inspired by https://github.com/inloop/cra-docker and was able to find a solution for the configuration file problem with create-react-app at runtime in both Docker and kubernetes. Below are the steps in my solution:

  1. Have your custom configuration (eg: config.js) ready. The contents inside your config file should look like this:

     window.ENV = {
         "ENVIRONMENT":"stg",
         ...other key-value configuration as you'll need
     }
    

    Your configurations will be accessible anywhere in your code by accessing window.ENV.your_configuration_key (eg: the ENVIRONMENT value above is available at window.ENV.ENVIRONMENT)

  2. Under public directory, edit the index.html and add

     <script type="text/javascript" src="%PUBLIC_URL%/config.js"></script>
    

    in your head before the body. And put config.js under public directory.

    Your goal in solving the external configuration for cra is that you want to put your config.js file outside source directory and put it under the static public directory. If you put the config under source directory, the config will get compiled during build time so you won't able to change the config during runtime easily (well, templating works too but it's not ideal to me). Do note that serving files from static directory require a server, but since I'm already using nginx anyway to serve my static react app, I have absolutely no trouble doing this.

  3. Since I was already using nginx to serve the static files for my react app, I don't need to make changes to my Dockerfile to cater for additional config.js as it will be available under the build directory after compiled (due to it being placed under public directory). My Dockerfile looks something like this:

     # Step 1: Build static react app
     FROM node AS builder
    
     # Define working directory and copy source
     WORKDIR /app
    
     COPY . .
    
     # Install dependencies and build whatever you have to build 
     RUN yarn install && yarn build
    
     # Step 2: Run image
     FROM nginx
    
     COPY --from=builder /app/build /usr/share/nginx/html
     RUN rm /etc/nginx/conf.d/default.conf
    
     COPY nginx.conf /etc/nginx # this step is not required, only when you have custom nginx configuration
    

    Then afterwards build your docker image:
    docker build -t [your-docker-image]:latest .

  4. Last but definitely not least, you'll want to replace your config.js file during runtime. This can now be done easily.

    If you're running using docker, you can replace files by using the -v command. So your docker runtime command should look something similar to this:

     docker run --name [app-container-name] -d -p [host_port]:80 \
         -v [path_to_config.js_file_in_certain_environment]:/usr/share/nginx/html/config.js \
     [your-docker-image]
    

    If you're running using kubernetes, you can replace files in an existing directory under your container by using configMap, volume, volumeMounts and subPath.

    • First put your config.js under k8s ConfigMap:

      kubectl create configmap [k8s_config_name] --from-file=config.js=[path_to_config.js_file_in_certain_environment]

    • Mount your configMap in your k8s deployment:

      containers:        
        ...
        volumeMounts:
        - name: [k8s_config_name_volume]
          readOnly: true
          mountPath: "/usr/share/nginx/html/config.js"
          subPath: "config.js"
      
      volumes:
        - name: [k8s_config_name_volume]
          configMap:
            name: [k8s_config_name]
      

    Note that both mountPath and subPath parameters are necessary to replace a file under a directory that already has some files existing in it. If subPath is omitted during volume mount to an existing directory which already contains some files the result is unfavourable in our case cos it will override the existing directory by copying the new file into the directory but removing all other previously existing files.

Alexander Nied
  • 12,804
  • 4
  • 25
  • 45
dekauliya
  • 1,303
  • 2
  • 15
  • 26
11

I've managed to hack together a solution.

in the public folder 'config.js'

var config = {
      x: 'y',
};

Next wrap the ReactDOM.render (App/index.js in a fucntion like so

window.RenderApp = (config) => {
   ReactDOM.render(<App _config={config}/>, document.getElementById('root'));
}

In the index.html add these lines, the window.RenderApp HAS to be at the end, because it relies on bundle.js being imported which is auto added by react and has a random name in production.

</html>
...
<head>
...
<script type="text/javascript" src="%PUBLIC_URL%/config.js"></script>
...
</head>
...
<body>
...
</body>
<script>
   window.RenderApp(config);
</script>
</html>

lastly to use the config variables in your App.js or what ever you called it

...
constructor(props) {
    super(props)
    console.log(this.props._config)
    this.state = {
       ....
       config: this.props._config,
   }
}
...

I found you have to set config to a state variables or else it will randomly throw undefined for the object, now just pass config down the hierarchy to use in your code.

Space Bear
  • 755
  • 1
  • 6
  • 18
8

We had a similar use case to - 'build once, deploy anywhere'. The following code seems to work for us:

There is config.js file in the public directory with the following code: const API_URL = '<API_URL>';

index.html file contains the following:

<script src="/config.js"></script>
<script>
(function (global) {
  var config = {
    API_URL
  };

  global.app = {
    env: config
  };
})(window);

This adds the variables from config.js into the global scope and can be accessed anywhere in the source code as : const API_ROOT = window.app && window.app.env.API_URL;.

Princi Ya
  • 131
  • 1
  • 3
5

I really like your approach with a config file that you can replace/update as needed. I think I may give it a go when I can find the time - thanks for sharing your approach!

The way I currently handle dynamic client configs is based on the URL the app is being accessed at - I use this to be able to move a bundle through our deployment pipeline (dev -> test -> staging -> production) without rebuilding the bundle.

env.js:

const rootUrl = `${location.protocol}//${location.host}`;

deployment-settings

const envs = {
  // Production
  'https://example.com': {
    APP_ENV: 'production',
    API_SERVER: 'https://api.example.io',
    INTERCOM_APP_ID: 'foo',
  },

  // Staging
  'https://staging.example.com': {
    APP_ENV: 'staging',
    API_SERVER: 'https://staging-api.example.com',
    INTERCOM_APP_ID: 'bar',
  },

  // Development
  'http://localhost:3000': {
    APP_ENV: 'development',
    API_SERVER: 'http://localhost:4000',
    INTERCOM_APP_ID: 'baz',
  },

};

if (envs[rootUrl]) {
  // Set environment variables based on the URL the app is being accessed from
  window.env = envs[rootUrl];
} else {
  // Redirect to production if the rootUrl is unknown
  window.location.replace(Object.keys(envs)[0]);
}
Mikael Lirbank
  • 4,355
  • 2
  • 28
  • 25
  • 1
    Thanks! but the problem I ran into is what I'm building will be deployed into an air gaped network and I have no idea how the sys admins will want it deployed. So I wanted a more "linux" approach of just having a config file somewhere on the file system they can configure without any knowledge of the code. – Space Bear Aug 03 '18 at 12:07
  • @Mikael Can you please help with how we can extend this configuration to be used in the react package.json file? I mean, applying build and start commands based on different environments? – program_bumble_bee Nov 04 '20 at 16:38
5

Here my solution.

Consider to have some environments where "you know configurations" and other environments where "you don't know configuration", but they must be configured by system administrators.

For example 3 environments: local and uat where you know configurations, and production where you don't know configuration to set.

So, I wan't that a config file is automatically generated for specific environment, and this config file must be editable

We can use a config.js file where put a json object.

So, first go in index.html and add in head

<script src="config.js"></script>

This means that config.js file must be in compiled root folder (same position of index.html), that means inside react solution "public" folder. But we want that this config.js is dynamic based on environment.

Consider to create in react solution root folder a folder called "config". Put in this folder 3 files: config_local.js, config_uat.js, config_prod.js

-config_local.js

var Configs = {
    API_ENDPOINT':"http://local:88/service"
 }

-config_uat.js

var Configs = {
 'API_ENDPOINT':"http://uat:88/service"
}

-config_prod.js

var Configs = {
 'API_ENDPOINT':""
}

We need to copy one of this 3 files inside root folder, renaming output file to "config.js"

So, first we need to have a .env file for each envirorment. In this env file we want just set the filename that will be copied in root folder.

So create a .env, .env.uat, .env.local, .env.prod in react root folder

-.env (this is default)

REACT_APP_CONFIG_FILE=config_prod.js

-.env.local

REACT_APP_CONFIG_FILE=config_local.js

-.env.uat

REACT_APP_CONFIG_FILE=config_uat.js

-.env.prod

REACT_APP_CONFIG_FILE=config_prod.js

We need to add/replace scripts object in package.json. Before we need to install env-cmd (npm install env-cmd). Here an example of scripts

"scripts": {
"start": "env-cmd -f ./.env react-app-rewired start",
"build": "env-cmd -f ./.env react-app-rewired build",
"test": "env-cmd -f ./.env react-app-rewired test",
"start:local": "env-cmd -f ./.env.local react-app-rewired start",
"build:local": "env-cmd -f ./.env.local react-app-rewired build",
"start:uat": "env-cmd -f ./.env.uat react-app-rewired start",
"build:uat": "env-cmd -f ./.env.uat react-app-rewired build",
"start:prod": "env-cmd -f ./.env.prod react-app-rewired start",
"build:prod": "env-cmd -f ./.env.prod react-app-rewired build"
},

PS: I've used react-app-rewired. I need it because we need to use a CopyFile plugin, but I can't set it in webpack, because my app is created with create-react-app. So, if you haven't webpack file, install rewired with npm install react-app-rewired

Now, we need to copy specific js file based on specific environment. So install copyfile with npm install copy-webpack-plugin Example if we don't have webpack:

Create in react root folder a file called "config-overrides.js" e copy this content:

const CopyWebpackPlugin = require('copy-webpack-plugin');
let internal_env = process.env.REACT_APP_CONFIG_FILE || 'config_prod.js';

module.exports = function override(config, env) {
  if (!config.plugins) {
   config.plugins = [];
 }
config.plugins.push(
  new CopyWebpackPlugin(
  [
    {
      from: 'config/' + internal_env,
      to: './config.js'
    }
  ])
 );
 return config;
};

So, in our react files, we can read configuration with:

//@ts-ignore
const apiEndpoint = Configs.API_ENDPOINT || ''

NB: if you use typescript, you need to ignore, because we can't import configs as module

So, now you can start or build using one of this, based on desired environment:

npm run start
npm run build
npm run start:local
npm run build:local
npm run start:uat
npm run build:uat 
npm run start:prod
npm run build:prod

When you can build, specific config.js is generated in root app folder and can be editable (for example by system administrators)

3

For anyone looking for simple solution,

  1. Create a config.js file in your public folder

  2. Create an object inside it ex:

    window['getConfig'] = { API_URL:'http://xyz:8080/', PROD_VALUE:'Y' }

  3. Then in your application (components/api call) use it directly as window['getConfig'].API_URL

Since config.js is in the public folder you can change the values on production as well

Brian Burns
  • 20,575
  • 8
  • 83
  • 77
Ayushi Keshri
  • 680
  • 7
  • 18
0

Base on this documentation page , react enable your app to consume environment variable. You just need to make sure your environment variable starts with REACT_APP_.

In addition you can declare your environment variable in files per environment at the root of the project

  • .env
  • .env.development
  • .env.prod
Genoud Magloire
  • 555
  • 6
  • 15