Setting up a React development environment (with Hot Module Replacement)

Preda W.
11 min readFeb 23, 2018

--

This article provides guidance for setting up a development environment specifically for React. You will find it helpful to refer to my tutorial repository on Github to help you set up your own project development environment. Please also refer to my tutorial repo to follow how I structured my files as this will not be covered below.

TL;DR refer to my repository on Github called hotModuleReplacementTutorial. The webpack, babel and Eslint configurations are set up in this repo along with the inclusion of all the dependencies in the package.json file.

Introduction

Just like a mechanic needs a workstation and the right tools available that supports his or her work, a developer needs a development environment conducive to building software. A development environment should be given high attention because it determines how well we can do our jobs through the development process. A mechanic with a higher quality work bench and reliable tools should be able to do a much better job than one without. In the same manner, having a robust development environment supports us to build better software.

We will cover setting up the following development tool kits:
1. Babel
2. Webpack (development with Hot Module Replacement)
3. Eslint (AirBnB config)

First thing to do to initialise your repo is to execute the following command in your terminal. This will create a package.json file that holds the metadata for your project.

npm init

Babel

A .babelrc file sits at the root of a directory and contains the specific configurations for transpiling (explained later). Here is an example of the file.

{
"presets": [
"react",
"es2015",
["env", {
"targets": {
"browsers": "last 2 versions",
"node": "current"
},
"loose": true,
"modules": false
}],
],
"plugins": [
"react-hot-loader/babel",
"transform-object-rest-spread"
],
"env": {
"test": {
"plugins": [
"transform-es2015-modules-commonjs"
]
},
"server": {
"plugins": [
"transform-es2015-modules-commonjs"
]
}
}
}

Presets

Defining presets in your .babelrc file tells Babel what particular language features will form your application’s code base. Babel can thus administer the appropriate transformations of your code to JavaScript, a process known as “transpiling” to widely used ES5 JavaScript.

With this in mind, you can create a .babelrc file which records the presets, plugins and environment specific configurations needed so that Babel knows how to transform your XML/HTML-like code to JavaScript.

React makes heavy use of ES6. Without ES6, your life as a React developer is made unnecessarily more complicated. However, the version of ECMAScript that most browsers support is ES5 so we use Babel to transpile our ES6 code base to ES5.

If you are building in React, you will want to work with JSX. JSX is an XML/HTML-like syntax used by React that allows a developer to combine HTML syntax with JavaScript code. This makes sense because the two work together to for your web application on the front-end. An MVC framework combines JavaScript for the model and control and HTML for the view. Note that you can use React without JSX but it will make developing a lot more tedious and cumbersome.

The two presets used by Babel by default are:
`es2015` — supports ES6
`react` — supports JSX

Under presets, we can also specify the “env” or environments that can read our transpiled code.

Plugins

Plugins are similar to presets, but are concerned with more specific features of a programming language. One can consider presets to be a set of plugins. We may want to include plugins that are additional to the presets we define in our .babelrc to include extra support.

For example, in the above plugins configuration, we include react-hot-loader/babel because hot-loader is used as part of the HMR process. It allows React components to be live reloaded without losing state when saving something new in our component’s code — more about this below.

There is also the transform-object-rest-spread which is very handy for use with Redux, where we want to create a new object from an existing state object, so that we can modify this new object. So instead of using:

Object.assign({}, existing state object)

and add new properties to the new object subsequently, we can use:

{ ...state, updated state properties}

Env (environment specific configuration)

Note that “Env” is included in the presets configuration. Jest is the environment for testing React. There may be times when we need to override how Jest works, like when using it in Node so we need Babel to transpile our `imports` to `requires`, and target our current node runtime.

Installation

Configuration is only half the story. The relevant dependencies need to be installed in your application’s repository via the terminal.

npm install [name of dependency as below] --savebabel-core
babel-loader
babel-preset-env
babel-preset-es2015
babel-preset-react
react-hot-loader@3.1.3
babel-plugin-transform-object-rest-spread
These options useful for some situations (not installed in my tutorial repo):
babel-plugin-transform-runtime (dev)
babel-preset-stage-2

Webpack

Due to the complexity of the code base written on the client side, an application will usually be modular with components written in separate files. This is good practice for organisation and readability. These separate files will all need to be aggregated for the web browser. A webpack is a module bundler that brings together JavaScript, CSS, image files etc with a set of loaders.

A webpack.config.js file will sit at the root of your directory.

Consider this development webpack configuration which includes Hot Module Replacement (HMR):

const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const SRC_DIR = path.resolve(__dirname, 'client/src');
const DIST_DIR = path.resolve(__dirname, 'client/dist');
module.exports = {
entry: [
'webpack-hot-middleware/client',
`${SRC_DIR}/index.jsx`
],
devtool: 'inline-source-map',
output: {
path: DIST_DIR,
filename: 'bundle.js'
},
devServer: {
hot: true,
publicPath: '/',
historyApiFallback: true,
inline: true
},
resolve: {
extensions: ['.js', '.jsx', '.json', '.css']
},
stats: {
colors: true,
reasons: true,
chunks: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
new ExtractTextPlugin({ filename: 'styles.css', allChunks: true })
],
module: {
loaders: [
{
test: /\.jsx$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css/,
loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' })
}
]
},
watch: true
}
// Building for staging or production
if (process.env.NODE_ENV === 'staging' || process.env.NODE_ENV === 'production') {
config.entry `${SRC_DIR}/index.jsx`;
config.devtool = false;
config.plugins = [];
}

HMR is a powerful development tool that lets us change components and have those changes reflect rapidly in the browser without needing a full refresh. Only the component that was changed gets replaced; other components are left alone. This is very useful because it allows your components to keep their state, while you change a small part of the application. You also do not have to go through the tedious task of rebuilding your application for a new bundle file each time you make a change. HMR swaps out the changed component for you if you configure the webpack to do so. The documentation of how HMR works can be accessed here. I really recommend reading this as each developer can determine if this is a tool for them.

Entry

The entry configuration indicates the point or points to enter the application. This tutorial is geared towards a single page application, so only one point (one string or one array) will be indicated.

Devtool

There are dev tools available for example helping to debug. Say for example webpack has bundled up your files and there is a bug that originated from one of the module files. The stack trace will simply lead to the bundle file as the source of the bug which is unhelpful for the developer. A source map is a developer tool that can be used to more easily track down errors and warnings. A source map will map your compiled code back to the original source code — pretty neat, isn’t it? The dev webpack file above includes one of the source maps — ‘inline-source-map’. Other options include ‘cheap-eval-source-map’; please refer to the webpack documentation for more details on source maps.

Output

The output configuration contains the instructions for webpack to know where to output your bundle file.

DevServer

The devServer configuration is where a developer sets the options that are picked up by the webpack-dev-server. The devServer.hot option enables webpack’s HMR feature. Note that if you choose to use HMR, the webpack.HotModuleReplacementPlugin is required to fully enable it. The documentation states that if the hot option is selected, “this plugin will be added automatically, so you may not need to add this”. However, for safety and completeness, I will add this to our configuration anyway.

The devServer.publicPath option allows a developer to explicitly indicate the directory that the bundle.js file is available. If this option is not included, the default directory for the bundle file is “/”.

When using the devServer.historyApiFallback option, “the index.html page will likely be served in place of any 404 responses.”

The devServer.inline option is “recommended for HMR as it includes the HMR trigger from the websocket.”

Resolve

When a developer builds React components and imports other component files, it is possible to leave off the extensions to the files e.g. ‘.jsx’ or ‘.js’ if the resolve configuration array is included in the webpack. An array as in the above webpack example specifies the order of the extensions to be applied to files if left off the import statements — very neat indeed.

Stats

When webpack builds your bundle file, the terminal can display more or less information about it. The stats configuration allows you to control what gets displayed. Please refer to webpack’s documentation for an explanation of all the different available options. The ones I use are included above.

Plugins

This lets you define your plugins in an array to customize your build process.

Modules

These options determine how the different types of modules within an application project will be treated.

Watch

If watch mode is turned on, after the initial build, webpack continues to watch for changes to the resolved files.

At the end of the webpack, there is a conditional that nullifies the development tools if the process environment is set to anything other than development, e.g. in staging or production mode the development options object is emptied.

Installation

The webpack dependencies for development are:

npm install [name of dependency as below] --savewebpack@3.11.0
extract-text-webpack-plugin@3.0.2
extract-text-webpack-plugin@next
webpack-dev-middleware@2.0.6
webpack-hot-middleware

npm install [name of dev-dependency below] --save-dev
webpack-dev-server@2.11.1

Eslint

Eslint is a terrific tool to make sure one’s code follows good practice. If we stray away from standard practice, our code becomes more difficult for others to follow. It encourages consistency of coding practices for individuals and within teams — highly recommended. For more detailed explanation and options, please refer to the Eslint documentation getting started guide.

If there is an Eslint error in your front end code, you will see a message similar to this below which should prompt you to correct your code.

Installation

Install the following modules to enable Eslint in your project using:

npm install [name of dependency as below] --save eslint
eslint-config-airbnb
eslint-config-prettier
eslint-config-react
eslint-loader
eslint-plugin-import
eslint-plugin-jsx-a11y
eslint-plugin-prettier
eslint-plugin-react
prop-types

Create two files at the root of your project:

touch [name of file as below].eslintrc
.eslintignore

In your .eslintrc file drop down the following:

{
"extends": [
"airbnb",
"prettier",
"prettier/react"
],
"plugins": [
"prettier"
],
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"es6": true,
"browser": true,
"node": true,
"jest": true
},
"rules": {
"import/no-named-as-default": 0
}
}

In your .eslintignore file drop the following to ignore certain files from lint checking as these do not need to be checked:

client/dist/
node_modules/

Further dependencies to install and considerations

npm install [name of dependency as below] --saveexpress
react
react-dom

Include a .gitignore file to ignore your node-modules as you do not want to commit these upstream.

Of course, you are going to need to create a client/dist/index.html file:

<!DOCTYPE html>
<html>
<head>
<title>HMR</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>

And an empty client/dist/styles.css file to begin with.

HMR and server-side rendering

Server-side rendering is faster than client side rendering, so I have served up my HTML from the server in my repo. If you serve up the HTML and CSS from the server, the browser can begin to render the page without having to wait for the other parts to make it complete such as images and your JS code.

Hopefully by now you will have structured your repository as I have done, with client folders and index files in the corresponding places as well as server folders and index file. In your server/index.js file, you will want to drop this down:

'use strict'require('babel-register'); // ensures everything in this file is run through babelconst express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const PORT = process.env.PORT || 4000;
const { createElement } = require('react');
const { renderToString } = require('react-dom/server');
const _ = require('lodash');
const App = require('../client/src/components/App.jsx').default;
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpackConfig = require('../webpack.config.js');
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler));
app.use(webpackHotMiddleware(compiler));
app.use(express.static(`${__dirname}/../client/dist`));
app.use((req, res) => {
const context = {}; // pass in context {} to body because sometimes you will get redirected.
const body = renderToString(createElement(App));
if (context.url) {
res.redirect(context.url);
}
const baseTemplate = `
<!DOCTYPE html>
<html>
<head>
<title>Hot Module Replacement</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div id="app"><%= body %></div> <!-- lodash templating for server-side rendering -->
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>
`;
const template = _.template(baseTemplate);
res.write(template({body}));
res.end();
});
app.use('*', (req, res) => {
res.status(404).send();
});
app.listen(PORT, err => {
err ? console.error('Error with server') : console.log(`Listening on port ${PORT}`);
});

Start developing

In your package.json file, ensure you have the following scripts to help you run your application faster.

"scripts": {
"start": "NODE_ENV=server nodemon server/index.js",
"build": "webpack -p"
},

In your client/src/index.jsx file you should start the entry for your application. A conditional is needed to check if a module is hot, i.e. the last one edited. Within this conditional, you should instruct a re-rendering of the application.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
const renderApp = Component => {
ReactDOM.render(<Component />, document.getElementById('app'));
};
renderApp(App);
if (module.hot) {
module.hot.accept('./components/App.jsx', () => {
const App2 = require('./components/App').default; // eslint-disable-line global-require
renderApp(App2);
});
}

Now you should have a working repository that is conducive to rapid development.

In the root of your directory within the terminal run the following CLI command:

// Build the bundle first time with this command then exit it with 'control' + C
npm run build
// Now run this command
npm start

Try building a few more client side components and amending them. You should not need to force a refresh in your browser for your changes to appear. HMR takes care of that for you by changing only the ones you changed since the last refresh, leaving other components with possibly valuable state to remain unchanged.

If you found my post or example repository helpful in anyway, please give me some claps. I’d really appreciate it!

--

--