Although Wepack already comes with two modes: development and production, It comes handy to have different environment variables for development and production and to store them in different files.
Let’s create .env file which will be our development environment variable file.
Creating the .env file
./.env
APP_NAME=Webpack React Tutorial GRAPHQL_URL=http://localhost:4001/graphql
As it looks like .env is not a standart JavaScriupt object so we can’t use the variables out of the box. We will need two modules Dotenv and Dotenv-expand.
Adding and configuring Dotenv to load variables into process.env.
Dotenv is module that loads environment variables from a .env
file into process.env
.
yarn add dotenv dotenv-expand
Let’s give it a try and print out GRAPHQL_URL
in the backend. Let’s do this using server-api.js config. All that we need to do is to ‘tell’ Dotenv module to load variables into process.env
./src/server-api.js
import WebpackDevServer from 'webpack-dev-server'; import webpack from 'webpack'; import config from './webpack.api.config.js'; require('dotenv').config(); console.log(">>>" + process.env.GRAPHQL_URL); const compiler = webpack(config); const server = new WebpackDevServer(compiler, { hot: true, publicPath: config.output.publicPath, historyApiFallback: true }); server.listen(8080, 'localhost', function() {});
Run the project and in the backend console you will see GraphQL url printed out.
Now, let’s create getEnvironmentConstants
helper method that will use Dotenv to load variables, and in addition we will add a filter that will load only these variables to the front end that we specify in frontendConstants
. This way important variables that we need in the backend like passwords to the database won’t be exposed in the source code.
./getEnvironmentConstants.js
const fs = require('fs'); // Load environment variables from these files const dotenvFiles = [ '.env' ]; // expose environment variables to the frontend const frontendConstants = [ 'APP_NAME', 'GRAPHQL_URL' ]; function getEnvironmentConstants() { dotenvFiles.forEach(dotenvFile => { if (fs.existsSync(dotenvFile)) { require('dotenv-expand')( require('dotenv').config({ path: dotenvFile, }) ); } }); const arrayToObject = (array) => array.reduce((obj, item, key) => { obj[item] = JSON.stringify(process.env[item]); return obj }, {}) return arrayToObject(frontendConstants); } module.exports = getEnvironmentConstants;
Once we have the object in place we could use the DefinePlugin to pass them to the frontend.
Adding the DefinePlugin.
if we go back in this tutorial we will remember that we showed how to configure Webpack in three different ways: using CLI, the webpack API and the server middleware.
Now the best place to add DefinePlugin will be in webpack.base.config so all three Webpack set-ups will take advantage of it. Let’s import getEnvironmentConstants and pass it as a parameter to DefinePlugin.
./webpack.base.config.js
import getEnvironmentConstants from './getEnvironmentConstants'; import webpack from 'webpack'; module.exports = { mode: 'development', devtool: 'eval-source-map', entry: [ '@babel/polyfill', './src/index.js', ], output: { filename: '[name]-bundle.js', publicPath: '/dist', }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader" } }, // SCSS { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, importLoaders: 2, localIdentName: '[folder]-[local]', sourceMap: true } }, { loader: 'postcss-loader', options: { plugins: () => [require('autoprefixer')()], sourceMap: true }, }, { loader: 'sass-loader', options: { outputStyle: 'expanded', sourceMap: true } } ], }, // images { test: /\.(png|jp(e*)g|svg)$/, use: [{ loader: 'url-loader', options: { limit: 8000, // Convert images < 8kb to base64 strings name: 'images/[hash]-[name].[ext]' } }] }, //File loader used to load fonts { test: /\.(woff|woff2|eot|ttf|otf)$/, use: ['file-loader'] } ] }, plugins: [ new webpack.DefinePlugin({ 'process.env' : getEnvironmentConstants() } ) ] };
what we just did:
– (line 1 and 2) we imported getEnvironmentConstants
and Webpack
since we will need it to instantiate the plug in.
– (line 72-74) we added the plug in.
We have to do one more change in order to have the plug-in working for all Webpack configs:
./webpack.api.config.js
const webpack = require('webpack'); let config = require('./webpack.base.config.js'); config.entry = [ '@babel/polyfill', './src/index.js', 'webpack/hot/dev-server', 'webpack-dev-server/client?http://localhost:8080/', ]; config.plugins = [... [new webpack.HotModuleReplacementPlugin()], ... config.plugins ]; module.exports = config;
and
./webpack.middleware.config
const webpack = require('webpack'); let config = require('./webpack.base.config.js'); config.entry = [ '@babel/polyfill', './src/index.js', 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000', ]; config.plugins = [... [new webpack.HotModuleReplacementPlugin()], ... config.plugins ]; module.exports = config;
what we just did:
Since we added config.plugins
array in webpack.base.config we don’t want to override it here and lose the changes. That’s why we are merging the array using spread operator.
If you are unfamiliar with the spread operator you might read the link above. Basically what it does is to ‘spread’ two or more arrays into the current array.
var a = [1,2,3]; var b = [...a, ...[4,5,6]]; console.log(b); result: (6) [1, 2, 3, 4, 5, 6]
Accessing env variables in the front-end.
And let’s load these variables. We could replace the hardcoded GraphQL url with the one from the .env file.
And using the variables on the back end is straight forward: we just include .env
file and use the variables, but passing them to the front end requires a little bit more effort. We have to use the DefinePlugin which will allow us to create global constants which can be configured at compile time.
./src/components/App/index.js
import React, { Component } from 'react'; import PageLayout from '../../containers/PageLayout'; import { ApolloProvider } from 'react-apollo'; import { ApolloClient } from 'apollo-client'; import { HttpLink } from 'apollo-link-http'; import { InMemoryCache } from 'apollo-cache-inmemory'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import { Provider } from 'react-redux'; import { createStore} from 'redux'; import reducers from '../../reducers'; import styles from './styles.scss'; const store = createStore(reducers, {}); export default class App extends Component { render() { const GRAPHQL_URL = process.env.GRAPHQL_URL; const client = new ApolloClient({ link: new HttpLink({ uri: GRAPHQL_URL }), cache: new InMemoryCache() }); return ( <div className={styles.appWrapper}> <Provider store={store}> <ApolloProvider client={client}> <Router> <Switch> <Route exact path="*" component={PageLayout} /> </Switch> </Router> </ApolloProvider> </Provider> </div> ); } }
And we could also print the APP name in the header section process.env.APP_NAME
./src/components/Header/index.js
import React from 'react'; import { Link } from 'react-router-dom'; const styles = require('./styles.scss'); const Header = ( {title} ) => ( <div> <div className={styles.wrapper}> <h2>{ title } { process.env.APP_NAME } </h2> <ul> <li><Link to='/home'>HOME</Link></li> <li><Link to='/greetings'>GREETINGS</Link></li> <li><Link to='/dogs-catalog'>DOGS CATALOG</Link></li> <li><Link to='/about'>ABOUT</Link></li> </ul> </div> </div> ); export default Header;
Now, start the server using yarn start-api
and if everything works fine you will see the “Webpack React Tutorial” in the header.