In a ReactJS app, how to substitute env vars from the global environment into my index.html file?

Viewed 1935

I have a ReactJS 17 app, in which I would like to substitute environment variables from my global environment into my src/index.html file. However, we're not using .env files, as this answer suggests -- Create React App: using environment variables in index.html . Here is the src/index.html file ...

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
    
    ...
    <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="512x512">
    <script src="%REACT_APP_THIRD_PARTY_URL%"></script>
  </head>
  <body>

In my shell, I can see the env var in my global environment

$ echo $REACT_APP_THIRD_PARTY_URL
http://thirdpartyurl.com

I am running my application in development mode, installing using npm i and running using npm run start. However, the substitution above is never made, and the page is getting rendered with

<script src="%REACT_APP_THIRD_PARTY_URL%"></script>

Edit:

This is a development environment, so hot re-loading and all of that. Here is how I "start" in package.json

"start": "webpack-dev-server --open --config webpack.development.js"

Edit 2: Not sure if this matters, but this is in my webpack file ...

my package.json ...

{
  "name": "my_app",
  "version": "1.0.0",
  "description": "My desc",
  "main": "index.js",
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watchAll",
    "test:coverage": "jest --coverage",
    "build": "webpack --config webpack.production.js",
    "start": "webpack-dev-server --open --config webpack.development.js",
...

Here's the webpack.development.js file ...

require('dotenv').config();
const { merge } = require('webpack-merge');
const common = require('./webpack.common');

const WEBPACK_WATCH_POLLING = process.env.WEBPACK_WATCH_POLLING === '1';

module.exports = merge(common, {
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist',
    publicPath: '/',
    historyApiFallback: true,
    disableHostCheck: true,
    hot: true,
    host: '0.0.0.0', 
    port: 8080,
  },
  output: {
    publicPath: '/',
  },
  // This is needed for docker-on-windows.
  watchOptions: !WEBPACK_WATCH_POLLING
    ? undefined
    : {
        poll: 1000,
      },
});

Here's my webpack.common file ...

module.exports = {
  entry: './src/index.tsx',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json', '.css', '.scss'],
    alias: {
      '@': path.resolve('src'),
      '@sb': path.resolve('.storybook'),
      '@static': path.resolve('static'),
      common: path.resolve('src/components/common'),
    },
  },
  module: {
    rules: [
      {
        test: /\.(js|ts)x?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            babelrc: true,
          },
        },
      },
      {
        test: /\.html$/,
        use: [
          {
            loader: 'html-loader',
          },
        ],
      },
      {
        test: /\.(scss|css)$/i,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
      {
        test: /\.(jpe?g|gif|png)$/,
        use: {
          loader: 'url-loader',
        },
      },
      {
        test: /\.svg$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[name].[ext]',
            outputPath: 'icons/',
          },
        },
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[name].[ext]',
            outputPath: 'fonts/',
          },
        },
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      REACT_APP_THIRD_PARTY_URL: "http://google.com"
      //filename: './index.html',
    }),
    new CopyWebpackPlugin({
      patterns: [
        { from: 'src/manifest.json' },
        { from: 'static/**/*.{png,svg,ico}', flatten: true },
      ],
    }),
    new WorkboxWebpackPlugin.InjectManifest({
      swSrc: './src/service-worker.js',
    }),
    new EnvironmentPlugin([
      'NODE_ENV',
      'REACT_APP_THIRD_PARTY_URL',
    ]),
  ],
};

And lastly, my src/index.tsx file ...

import './monitor';
import * as serviceWorker from './sw-registration';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app/App';
import './i18n.js';

ReactDOM.render(
  <React.StrictMode>
    <React.Suspense fallback={<div>Loading...</div>}>
      <App />
    </React.Suspense>
  </React.StrictMode>,
  document.getElementById('root')
);

serviceWorker.register();
2 Answers

There are a couple of possible approaches depending on your environment and build tools.

I suppose the most convenient approach for you will be to supply them to your project at build time.

  1. You can generate your index.html file dynamically(by using a template engine or a simple script that executes the job for you) and append the required variables manually by reading them from the environment.
  2. Or the more elegant solution - integrate this step into your build tools. If you're using webpack to build your bundle, you can use the HtmlWebpackPlugin to build your index.html file for you fed with all the env data you need.

In order to do that, create a simple .ejs or .html file that you're gonna use as a template:

<!DOCTYPE html>
<html>
   <head>
      ...
      <script src="<%= htmlWebpackPlugin.options.thirdPartyData.url %>"></script>
   </head>
   <body></body>
</html>

Then, in your webpack.config.js you setup the HtmlWebpackPlugin like so:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    ...
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: '<PATH_TO_YOUR_TEMPLATE>',
            thirdPartyData: {
                url: process.env.<YOUR_ENV_VARIABLE>
            }
        })
    ]
};

Webpack will fetch the variables from the environment(through node) and will build your index.html for you based on your template file. You can supply arbitrary data to the plugin which you can then read in the templates from the htmlWebpackPlugin.options property. You can learn more about HtmlWebpackPlugin here.

Take note that in order to change the environment variables in the code itself, you'll have to do a rebuild through webpack, which is understandable.

This solution doesn't require you to use create-react-app or .env files, nor does it require a server/server-rendering to do the job for you. Take note that it also works completely outside the context of the react app(you're not in the react app itself, you're working directly over the .html markup).


Another possible approach is to supply the environment variables through npm scripts using the --env flag but this will be inconvenient for you to integrate as it will require some additional work.


EDIT after comments:

Here is a minimal sample sandbox. The provided code won't work inside codesandbox itself due to the way it executes the scripts but if you duplicate the provided example on your machine it will do what it is supposed to. In order to test it, you simply need the index.html, the webpack.config.json, the package.json and a dummy index.js. I've striped out the react related code for brevity.

Simply download the codesandbox sample, run npm install and then npm start.

Then you can migrate the approach to your specific code base.


EDIT 2:

Okay, I found the issue. Your html-loader interferes with the HtmlWebpackPlugin and doesn't substitute the variables in the template. You'll notice if you comment out the html-loader in your webpack.config that the transformation works. So, the simplest solution will be:

  1. Create a new index.html.ejs file and put it next to your index.html. This is an actual proper ejs template. Put the current contents of your index.html there:

index.html.ejs:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
    <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="512x512">
    <script src="<%= htmlWebpackPlugin.options.REACT_APP_THIRD_PARTY_URL %>"></script>
    ...
</head>

<body>
    ...
</body>

</html>
  1. In your webpack.common.js point the template property in HtmlWebpackPlugin config to lead to that template:
new HtmlWebpackPlugin({
    template: './src/index.html.ejs',
    REACT_APP_THIRD_PARTY_URL: "http://google.com"
})

At this point you can even delete the index.html altogether, as it is going to get generated by the HtmlWebpackPlugin plugin.

This way you preserve your html-loader transformations(if you actually need it for anything) and you get the interpolated html file.

You can add temporary environment variables in your shell which your package manager (npm or yarn) can use to build the React app.


You can do it in these ways:

  1. By adding a temporary variable:
$ REACT_APP_THIRD_PARTY_URL=http://thirdpartyurl.com npm start

Or

  1. By adding a temporary variable (but from a global shell variable) :
$ REACT_APP_THIRD_PARTY_URL=$REACT_APP_THIRD_PARTY_URL npm start

Assuming that "REACT_APP_THIRD_PARTY_URL" is defined as global env variable (prints a value for $ echo $REACT_APP_THIRD_PARTY_URL)


You can similarly run other npm / yarn commands such as "build", "test" etc. And it will replace the value in HTML (<script src="%REACT_APP_THIRD_PARTY_URL%"></script>).

There are subtle differences when using different Operating Systems. Above examples are for Linux / Macos Bash shell.

Edit:

If you don't want to write these long env vars each time with npm start or build command, you can write these in package.json:

"scripts": {
    "start": "REACT_APP_THIRD_PARTY_URL=$REACT_APP_THIRD_PARTY_URL REACT_APP_ANOTHER_VAR=$REACT_APP_ANOTHER_VAR react-scripts start",
    ...
  }

If you are not using react-scripts but using webpack-dev-server directly, here is how to achieve the same:

  1. Write this in your html file:
<script src="<%= htmlWebpackPlugin.options.REACT_APP_THIRD_PARTY_URL %>"></script>
  1. Add "REACT_APP_THIRD_PARTY_URL" in HtmlWebpackPlugin options:
const appDirectory = fs.realpathSync(process.cwd());
const resolveAppPath = (relativePath) => path.resolve(appDirectory, relativePath);

plugins: [
    new HtmlWebpackPlugin({
      inject: true,
      template: "./src/index.html", // OR resolveAppPath("src/index.html")
      REACT_APP_THIRD_PARTY_URL: process.env.REACT_APP_THIRD_PARTY_URL, // HERE
    }),
  ],
  // ...
  1. Do nothing in your package.json script. Let it be like it was, maybe:
"start": "webpack-dev-server --open --config webpack.development.js"

Related