React - starting from scratch with Webpack, Babel and ESLint

Contents

  1. Introduction
  2. Webpack
  3. Webpack-dev-server
  4. Styles
  5. ES6/ES7
  6. React
  7. ESLint
  8. Source Maps
  9. Production Build

What is this about

About a month ago, I decided to learn React and begun reading Getting started articles on the subject. It soon became apparent that what I was embarking on was not just learning React, but also other tools like Webpack, Babel or Gulp.

Boilerplates and Generators

It was overwhelming to learn all those tools in parallel, so I settled for one of these boilerplate React projects. I recently learned that there is a generator from Facebook called create-react-app. If you want to read more about this kind of generators - Andrew Farmer's post is one of the best resources I found.

Starting from scratch

Starting from scratch may seem weird if you're coming from Rails or Ember, where the frameworks already come with app generators.

Personally, I see React & ecosystem as Sinatra - one can either utilize boilerplates/generators, or start from scratch and piece together all the required libraries.

Hopefully, in the future React will get an official react-cli (similar with ember-cli). But this is a completely different discussion. In the meantime, I think starting from scratch is a better approach if you're just getting started.

My starting point was Jonathan Verrecchia's JavaScript Stack from Scratch Github tutorial. I learned a lot by following along, but I was also impatient. My focus was on getting the set-up out of the way as quickly as possible, so I could start coding my learning React app. I skimmed through the guide, without taking the time to learn Webpack and Gulp. Jonathan's guide is just a foundation, which one would needto build on top of for things like:

  • Live code reloading
  • Stylesheets
  • Production builds with minification
  • etc.

In hindsight, this is the plan I wish I followed for mastering React:

  • use only webpack to start, instead of webpack+gulp (one less thing to learn)
  • use webpack-dev-server and hot-module-replacement for live updates in the browser
  • add ESLint loader that runs on every update
  • process both, vendor css and my custom css
  • create a production build.

Webpack

Let's start with the basics. Webpack is a module bundler that can be installed globally or as part of a project, using NPM.

npm init
npm install --save-dev webpack

Let's create the bare minimum webpack.config.js file that will bundle an source.js file into an output:

module.exports = {
    entry: "./entry.js",
    output: {
        path: __dirname,
        filename: "bundle.js"
    }
};

The entry can be a string or an array. It is the starting point for Webpack to follow imports and bundle everything together into the output specified. For simplicity's sake at this step, there is only a path pointing to the current folder and a filename.

This is our entry.js file:

console.log('Hello World!');

Next, let's create an index.html file and load the bundle.js file.

<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
   <script type="text/javascript" src="bundle.js" charset="utf-8"></script>
   </body>
</html>

Running Webpack generates the bundle.js as expected. Notice that Webpack generates a hash for every build - this will come in handy when dealing with asset caching.

$ webpack
Hash: 26a59627d47eefca08e4
Version: webpack 1.14.0
Time: 42ms
    Asset     Size  Chunks             Chunk Names
bundle.js  1.42 kB       0  [emitted]  main
   [0] ./entry.js 29 bytes {0} [built]

External Modules

Here, let's pretend to write a module and import it into the entry.js file to verify how Webpack bundles that.

Create a new file and call it message.js

module.exports = {         
  sayHello: function() {   
    return "Hello World!";
  }
};

Update the entry.js file:

  var message = require('./message');

  document.write(message.sayHello());

Now if we open index.html we should see 'Hello World!'.

Webpack-dev-server

It is inconvenient to repeatedly run webpack and refresh the index.html file. Instead, let's set up Webpack to watch for changes in the files and bundle those with every change. Webpack-dev-server runs a simple express app to serve content and refresh listens for changes on the files using WebSockets.

Install webpack-dev-server:

npm install --save-dev webpack-dev-server

The node server will run on port 8080 by default. This can be changed in the webpack.config.js:

module.exports = {         
  entry: './entry.js',     
  output: {
    path: __dirname,       
    filename: 'bundle.js'  
  },
  devServer: {             
    port: 8080             
  },
};

Now I can run webpack-dev-server and browse to http://localhost:8181 to see the app.

Let's add the --hot option to enable Hot Module Replacements (this means that adding, removing or updating modules will not require restarts and will automatically reload the page). The --inline option includes the webpack-dev-server in the generated bundle.js file:

webpack-dev-server --hot --inline

Bam! The live updates are now available!

Finally, add this to the package.js scripts:

{
  "name": "webpack_blogpost",
  "version": "1.0.0",
  "description": "", 
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server --hot --inline"
  },  
  "author": "", 
  "license": "ISC",
  "devDependencies": {
    "webpack": "^1.14.0",
    "webpack-dev-server": "^1.16.2"
  }
}

Styles

So far there was no need to use loaders to handle the JS content, because we only had a simple entry file with a loaded module. Next, let's look at two loaders to bundle up the stylesheets: style-loader and css-loader. The first loader injects a <style> element in the served content (from webpack-dev-server) and the latter resolves imports, similar to how webpack resolves js module imports.

npm install --save-dev css-loader style-loader

Add two types of stylesheets typically used in a React app: a custom one for the app and a framework, such as Bootstrap.

Let's create the app.css :

body {
  background: #efefef;
}

Now update the webpack.config.js and add the css and style loaders:

module.exports = { 
  entry: './entry.js',
  output: {
    path: __dirname,
    filename: 'bundle.js'
  },  
  module: {
    loaders: [
      {   
        test: /\.css$/,
        loader: "style!css"
      }   
    ]   
  },  
  devServer: {
    port: 8181
  },  
};
~ 

Install Bootstrap (as of this writing, the latest alpha release for v4 is alpha.6).

npm install --save bootstrap@4.0.0-alpha.6

Now I can require both, bootstrap and the stylesheet, in the entry.js file:

var message = require('./message'); 

require('bootstrap/dist/css/bootstrap.css');
require('./app.css');      

document.write(message.sayHello());

Running npm start and checking the result, I see two <style> tags - one for each stylesheet imported.

Note that now the bundle.js almost doubled in size (from about 270Kb to about 470kB). Keeping in mind that none of the CSS code was minified, this is still a massive jump to be be aware of. In practice, this will probably not occur and the vendor css would be bundled separately - more about this in the Production Build section

ES6

With the working Webpack app, it is a good time to use some of the nicer ES6 features and update the code. Use Babel to compile the ES6 enabled code to JS code, which most browsers can run.

npm install --save-dev babel-loader babel-core babel-preset-latest

As different versions of Javascript get approved by ECMA International, they are given nicknames, such as ES2015 and ES2016, or the latest ES2017. Babel's latest-preset page offers more up to date details. These presets are specified using a query object as part of the loader.

Similar to how the style and css loaders were used, let's use the babel-loader to process any of the matched files in the test regular expression.

Here is the updated webpack.config.js:

module.exports = { 
  entry: './entry.js',
  output: {
    path: __dirname,
    filename: 'bundle.js'
  },  
  module: {
    loaders: [
      {   
        test: /\.css$/,
        loader: "style!css"
      },  
      {   
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        query: {
          'presets': ['latest']
        }
      }   
    ]   
  },  
  devServer: {
    port: 8181
  },  
};

Now let's change all the requires to imports:

var message = require("./message");
require("./app.css");

to produce:

import message from './message';
import './app.css';

React

It is finally time to add React to the app. Let's install all the required modules:

npm install --save react react-dom

Next, install the React loader and the Babel preset for compiling React:

npm install --save-dev react-hot-loader babel-preset-react

Now, update the webpack.config.js and add the loaders and Babel presets:

module.exports = { 
  entry: './entry.js',
  output: {
    path: __dirname,
    filename: 'bundle.js'
  },  
  module: {
    loaders: [
      {   
        test: /\.css$/,
        loader: 'style!css'
      },  
      {   
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loaders: ['react-hot', 'babel-loader?presets[]=latest,presets[]=react'],
      }   
    ]   
  },  
  devServer: {
    port: 8181
  },  
};

Note that I removed the query from the jsx loader, because this can only be used with one loader. After adding the react-hot, let's define the presets inline. If the presets become more complex, refer to something like webpack-combine-loaders.

Let's create a sample component called App.jsx and import it. Also, use render code and update the HTML element, where the React app will mount.

// App.jsx
import React from 'react';

export default class App extends React.Component {
  render() {
    return (
      <button className="btn btn-primary">Press me!</button>
    );  
  }
}
// entry.js
import { render } from 'react-dom';
import React from 'react';

import App from './App.jsx';
import 'bootstrap/dist/css/bootstrap.css';
import './app.css';

render(
  <App />, 
  document.getElementById('app')
);
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <div id="app"></div>
    <script type="text/javascript" src="bundle.js" charset="utf-8"></script>
  </body>
</html>

Everything should bundle without errors and the Bootstrap button should be rendered in the browser.

ESLint

npm install --save-dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-loader eslint-plugin-react

Next, add a loader to Webpack and create the ESLint config file (but keep it separate from the Webpack config - .eslintrc)

// webpack.config.js

module.exports = {
  entry: './entry.js',
  output: {
    path: __dirname,
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.css$/,
        loader: 'style!css'
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loaders: ['react-hot', 'babel-loader?presets[]=latest,presets[]=react'],
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loaders: ['eslint-loader']
      }
    ]
  },
  resolve: {
    extensions: ['', '.js', '.jsx']
  },
  devServer: {
    port: 8181
  },
};

// .eslintrc

{
  "extends": "airbnb",
  "env": {
    "browser": true,
  },
  "plugins": [
    "react"
  ]
}

At this point, running npm start generates a handful of errors in entry.js and App.jsx:

entry.js
   4:17  error  Unexpected use of file extension "jsx" for "./App.jsx"  import/extensions
   5:8   error  Absolute imports should come before relative imports    import/first
   9:3   error  JSX not allowed in files with extension '.js'           react/jsx-filename-extension
  10:33  error  Missing trailing comma                                  comma-dangle

App.jsx
  3:16  error  Component should be written as a pure function  react/prefer-stateless-function

Go ahead and fix these errors. Refer to my repo for the current state of the app. I updated my Webpack script to build the app into its own folder and added it to my .gitignore.

build
src
 |_ app.css
 |_ App.jsx
 |_ entry.jsx
 |_ index.html
 |_ message.js
package.json
webpack.config.js
.gitignore
.eslintrc
.babelrc

This is my new folder structure. Note that the index.html needs to be copied over to the build folder. I have two options for that:

Using first option:

npm install --save-dev file-loader

Remember how the style loaders worked? The file loader looked for import statements matching a file, but instead of bundling the contents into the index file it copied that file to the build location:

import { render } from 'react-dom';
import React from 'react';
import 'bootstrap/dist/css/bootstrap.css';

import App from './App';
import './styles/app.css';
import './index.html'

render(
  <App />, 
  document.getElementById('app'),
);
// webpack.config.js

...snip

{
  test: /\.html$/,   
  loader: 'file?name=[name].[ext]'
}

...snip

Going forward, the loader will copy any html file imported into the app to the output folder. There is no change when running the webpack-dev-server, because everything is stored in memory. However, running webpack in the app folder will generate the build folder along with the index.html file.

Source Maps

One last thing to add before moving on to the Production build is the source maps for the bundled JS files.

When bundling everything and running JS in the browser, I cannot detect the exact line in the source code in case of an error. Source maps solve this problem by creating separate files for either JS or CSS.

// webpack.config.js
module.exports = {
  ...snip
  devtool: 'source-map',
  ...snip
}

Production Build

Let's create a separate Webpack config for production and call it webpack.production.config.js:

module.exports = {
  entry: './src/entry.jsx',
  devtool: 'cheap-module-source-map',
  output: {
    path: __dirname + '/build',
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.css$/,
        loader: 'style!css'
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loaders: ['react-hot', 'babel-loader?presets[]=latest,presets[]=react'],
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loaders: ['eslint-loader']
      },
      {
        test: /\.html$/,
        loaders: ['file?name=[name].[ext]']
      }
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify('production')
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      minimize: true,
      warnings: false,
    })
  ],
  resolve: {
    extensions: ['', '.js', '.jsx']
  }
};

Here, minify the bundled JS, set the environment to production and use a different source map. The cheap-module-source-map is better for production bundling, according to the official docs:

cheap-module-source-map - A SourceMap without column-mappings. SourceMaps from loaders are simplified to a single mapping per line.

Recalling how the stylesheets were used previously, I do not recommend bundling them together with the JS in the production build. Instead, let's extract all CSS files and output a separate minified bundle.css. Use a Webpack plugin called ExtractTextPlugin.

This is the final webpack config. Just add the link tobundle.css to the src/index.html file and...done!

// webpack.production.config.js

var webpack = require('webpack');
var ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = { 
  entry: './src/entry.jsx',
  devtool: 'cheap-module-source-map',
  output: {
    path: __dirname + '/build',
    publicPath: __dirname + '/build/static',
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract("style-loader", "css-loader")
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loaders: ['react-hot', 'babel-loader?presets[]=latest,presets[]=react'],
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loaders: ['eslint-loader']
      },
      {
        test: /\.html$/,
        loaders: ['file?name=[name].[ext]']
      }
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify('production')
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      minimize: true,
      warnings: false,
    }),
    new ExtractTextPlugin("bundle.css")
  ]
  resolve: {
    extensions: ['', '.js', '.jsx']
  }
};

Conclusion

Looking back, I can now see why some prefer to use Gulp and Webpack together. The last step, where the css was bundled together, would have been a better fit for a Gulp task. My next post will focus on adding Gulp to the mix and improving the setup.

Overall, I believe it is good practice to start simple and progressively add tools as needed. Gulp is a great example, along with== more improvements / tools I could have used.

Hope you enjoyed this as much as I did! Here's the Github repo with the final application:

https://github.com/tzumby/react-tutorial