Build a React Application from First Principles - Webpack

This is Part 9 of the series - Build a React Application from First Principles.

Get the completed code from the last step on GitHub and follow along!

In the last article, we made our application more modular, and used SystemJS to load those modules on the client-side.

However, because SystemJS loads modules on the client, when it encounters a dependency that it needs, it must make a separate request for it. This can lead to making thousands of requests in order to load a single page. For our relatively simple application, we are already making ~300 requests to load the home page.

Note on HTTP/2

With HTTP/2, this is not such an issue, as multiplexing allows all those HTTP requests to be transferred over the same TCP connection and multiple request and response messages can be in-flight simultaneously, which makes it much more performant as establishing TCP connections and queuing requests one after another is relatively slow.

Note on HTTP and TCP

HTTP is an application-level protocol, which means it facilitates communication between applications. To actually transfer HTTP messages over the internet, we use another protocol - TCP. TCP facilitates communication between hosts. HTTP messages are carried inside TCP messages, which gets transferred between hosts. HTTP is said to be layered over TCP.

With HTTP/1.x, to transfer a single HTTP message from the client to server, and vice versa, two hosts must establish a TCP connection using a three-way handshake (SYN, SYN-ACK, ACK), and after the TCP message containing the HTTP message is transfer, also terminate the connection.

With HTTP/2, those HTTP messages can be carried over the same TCP connection.

To use HTTP/2, both the browser and the server would need to support it. Making the server support HTTP/2 is relatively easy, and browser support for HTTP/2 is good.

Can I Use http2? Data on support for the http2 feature across the major browsers from caniuse.com.

However, a significant segment of browser usage are still on HTTP/1.x browsers; making thousands of TCP connections each page load is unacceptable.

RequireJS is another module loader that works in a similar fashion to SystemJS, but loads only modules in AMD format.

Module Bundling

Currently, the most popular approach is to bundle the application, alongside all its dependencies into one (or a small number) bundled JavaScript file. We'd then serve that bundled file over to the client. This has several benefits:

  • Instead of establishing thousands of connections, we only have a few
  • All the dependencies are resolved ahead of time (during the process of bundling the dependencies), and so the time required to resolve dependencies are eliminated
  • Further processing steps can be included apart from simple bundling - such as transpiling (e.g. from TypeScript / CoffeeScript / ES6 to ES5), uglifying, image optimization etc.
  • After then initial load, because all the code are already sent, transition between screens / pages would be lightening-quick
  • All the dependencies are resolved ahead of time, and can be tested before deployment, eliminating the risk of using a newer version of a module that is incompatible with another module. (Although an npm shrinkwrap or yarn.lock file could (and should) be used to achieve the same result)

However, bundling also has several disadvantages:

  • The entire application and its dependencies are transferred to the client, even if only a small part of it is required to render the first screen. This can lead to a long time to first render.
  • It does not follow the Loader specification, which will likely become the specification that standardizes how modules are loaded.

jspm offers bundling capabilities as well as other production optimizations such as code splitting, but the amount of documentation around it is sparse. Furthermore, whilst SystemJS / jspm holds great promise, they both has not yet reached a level of maturity expected of production-ready applications - SystemJS is currently on 0.19.42, and jspm on 0.16.48. The fact that they are still at 0.x versions means there're likely to be many breaking changes coming, as already experienced by some.

Angular 2 and Aurelia encourages the use of SystemJS and jspm, so alongside increasing support for HTTP/2, and the gradual push towards maturity, SystemJS and jspm might may see a surge in adoption in the next year or so, but for now, we do not believe it to be the best choice for a production-ready application.

So what's a more mature alternative? Webpack is, by a country mile, the most popular module bundler used in the React ecosystem. Webpack 2.2 is officially in final release since 17 Jan 2017, and with it comes new optimization features such as tree shaking, so there's no better time to introduce it that now.

Webpack

Webpack logo

We have already written an article on Webpack 1 - Getting Started with Webpack - so feel free to read that article as well as this one.

Entry / Output

As mentioned already, Webpack is a module bundler - it takes your application, and all its dependencies, be it JavaScript, HTML, CSS or files, and bundles them into one or a small number of files. These files can then be transferred to the client where they are executed to run your application.

More formally, it takes many source input files, and bundles them into output file(s). With Webpack, the developer specifies one or several entry points, and Webpack will follow require statements in each file to build up a tree of dependencies. It will then download those dependencies, and bundle them in the right order. This means that:

  • Orphan files or modules that are not depended on by others are not included in the bundle
  • Dependencies are organized in a tree-like structure, which allows Webpack to determine which modules are depended on by others, and include them first in the bundle. This results in dependencies being included the correct order
  • Modules which are depended on by more than one module are only included once

Switching to Webpack

Since its conception, it has expanded into more than a bundler, and became a Swiss army knife that eats into the market of stand-alone task runners and transpilers. We will get into those later, but first, let's start by converting our project from using SystemJS to Webpack.

Removing jspm and SystemJS

First, let's break everything and remove jspm.

$ yarn remove jspm

Also remember to remove the jspm property from package.json


{
  "devDependencies": {
    "webpack": "beta"
  },
  "jspm": {
    "name": "grapevine",
    "main": "grapevine.js",
    "dependencies": {
      "babel-preset-react": "npm:babel-preset-react@^6.16.0",
      "plugin-babel": "npm:systemjs-plugin-babel@^0.0.18"
    },
    "devDependencies": {},
    "peerDependencies": {
      "fs": "npm:jspm-nodelibs-fs@^0.2.0",
      "path": "npm:jspm-nodelibs-path@^0.2.0",
      "process": "npm:jspm-nodelibs-process@^0.2.0"
    },
    "overrides": {
      "npm:lodash@4.17.4": {
        "map": {
          "buffer": "@empty",
          "process": "@empty"
        }
      }
    }
  }
}

We also have no use for the jspm.config.js file, nor the jspm_packages directory, anymore.

$ rm -r jspm*

It might also be good to remove the node_modules directory and yarn.lock file, as there are many packages downloaded that we don't need anymore. It might feel cleaner just to regenerate them from a new package.json file.

$ rm -r node_modules/ yarn.lock

And perhaps the most drastic change is removing all the script tags from index.html. The dependencies for React, ReactDOM and Firebase would now be included in the bundle, and we don't need SystemJS and jspm.config.js files anymore.

The code in the <body> responsible for configuring Firebase and rendering our component onto the target would also be part of the application that gets bundled together. So we can move them to a temporary file and add them back in later.


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Grapevine</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react-dom.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js"></script>
  <script src="https://www.gstatic.com/firebasejs/3.6.4/firebase.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.41/system.js"></script>
  <script src="./jspm.config.js"></script>
</head>
<body>
  <div id="renderTarget"></div>
  <script>
    var config = {
      apiKey: "AIzaSyAKZQWBbnb5N3Ovkx5afBQuvnyVudSobwo",
      authDomain: "grapevine-84264.firebaseapp.com",
      databaseURL: "https://grapevine-84264.firebaseio.com",
      storageBucket: "grapevine-84264.appspot.com",
      messagingSenderId: "209626311092"
    };
    firebase.initializeApp(config);
  </script>
  <script type="text/babel">
    SystemJS.config({
      meta: {
        '*.jsx': {
          'loader': 'plugin-babel',
          'format': 'cjs',
          'babelOptions': {
            'modularRuntime': false,
            stage1: true,
            presets: ['babel-preset-react']
          }
        }
      },
    });
    var App = SystemJS.import('./components/App.jsx').then(App => {
      ReactDOM.render(<App />, document.getElementById('renderTarget'));
    });
  </script>
</body>
</html>

And we've ended up with the same barebones index.html that we started with.

<!DOCTYPE html>  
<html lang="en">  
<head>  
  <meta charset="UTF-8">
  <title>Grapevine</title>
</head>  
<body>  
  <div id="renderTarget"></div>
</body>  
</html>  
Installing Webpack and Re-Installing Dependencies

Next, we need to install Webpack. You can install Webpack globally, which allows you to bundle files anywhere in your system. However, we should install it locally, since we want our build process to be consistent, no matter the configuration of the machine it is ran on, so the version of Webpack used should be in the code (in the package.json and yarn.lock file. We should also install it as a developmental dependency, as it is only used in the build process, and has no used being transferred to the client.

$ yarn add webpack@beta --dev

We use the beta tag to get the latest version - 2.2.0, without it, we'd still be downloading the 1.x version.

React, ReactDOM, Babel, the Babel React preset, and Firebase all have their npm equivalents, so let's install them now.

$ yarn add react react-dom firebase; yarn add babel-loader babel-core babel-preset-react babel-preset-es2015 --dev

As you'd remember, Webpack takes in a set of input files and generates another set of output files. So, let's move our components/ directory into a src directory; also create a dist/ directory to house the generated files.

$ mkdir src dist; mv components/ src/

Next, we need to create an src/index.js file that acts as our entry point; since it is the first file that gets read, we'll place the React.DOM.render call and Firebase configuration there.

$ cd src/; touch index.jsx
var React = require('react');  
var ReactDOM = require('react-dom');  
var firebase = require('firebase');  
var App = require('./components/App.jsx');

var config = {  
  apiKey: "AIzaSyAKZQWBbnb5N3Ovkx5afBQuvnyVudSobwo",
  authDomain: "grapevine-84264.firebaseapp.com",
  databaseURL: "https://grapevine-84264.firebaseio.com",
  storageBucket: "grapevine-84264.appspot.com",
  messagingSenderId: "209626311092"
};
firebase.initializeApp(config);

ReactDOM.render(<App />, document.getElementById('renderTarget'));  

Lastly, since React, ReactDOM and firebase are not available globally, we need to require them inside each file where they are used. Add this to the top of each of the components (.jsx) files. You can omit the Firebase line for Header.jsx, since Firebase is not used in that component.

var React = require('react');  
var ReactDOM = require('react-dom');  
var firebase = require('firebase');  
Setting up webpack.config.js

One of the biggest difference between Webpack and other build systems is that it is un-opinionated, and everything is configurable.

There are too many a lot of build systems / task runners - Broccoli, Browserify, Brunch, Duo, Fly, Gobble, Grunt, Gulp, Jake, Rollup, Sprockets, Start, but Webpack is the most popular. Rollup is an up-and-coming module bundler, which is similar to Webpack, but bundles ECMAScript modules out-of-the-box.

This means the configuration are usually quite big. For now, all we want to do is to bundle files, so the following is all we need.

const webpack = require('webpack');

module.exports = {  
  entry: {
    app: './src/index.jsx',
  },
  output: {
    filename: './dist/bundle.js',
  },
};

Now, let's try bundling the files.

$ ./node_modules/webpack/bin/webpack.js

Note we are using the local version of webpack.

We can see that an bundle.js is generated in the dist/ directory, but it is throwing the following error.

$ ./node_modules/webpack/bin/webpack.js
Hash: ebcc41859310e2f47c56  
Version: webpack 2.2.0  
Time: 113ms  
           Asset       Size  Chunks             Chunk Names
./dist/bundle.js  799 bytes       0  [emitted]  app
[0] ./src/index.jsx 288 bytes {0} [built] [failed] [1 error]

ERROR in ./src/index.jsx  
Module parse failed: /home/administrator/sandbox/grapevine/src/index.jsx Unexpected token (14:16)  
You may need an appropriate loader to handle this file type.  
| firebase.initializeApp(config);
|
| ReactDOM.render(<App />, document.getElementById('renderTarget'));

The important part of that error message is Module parse failed: /home/administrator/sandbox/grapevine/src/index.jsx Unexpected token (14:16). If we look at line 14 of src/index.jsx, we can see that it is the line with JSX syntax that throws the error.

Using Loaders to Transpile JSX

So similar to how we used SystemJS.config to tell SystemJS to use plugin-babel to load .jsx files, we can do an almost identical setup for Webpack with loaders. And as with everything, we do this in the webpack.config.js.


const webpack = require('webpack');

module.exports = {
  entry: {
    app: './src/index.jsx',
  },
  output: {
    filename: './dist/bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader",
            options: {
              "presets": ["es2015", "react"]
            }
          }
        ]
      }
    ]
  },
};

Loaders are transformation programs that runs on the source files individually. For example, you'd use loaders to transform CoffeeScript / TypeScript into ES5 before bundling them; in our case, we use it to transform JSX into ES5.

The test property defines a regular express (RegEx), which if matched, would run this loader on the matched files. The exclude property defines all the directories that Webpack should not consider.

If a file does match the test RegEx, then the use property defines all the loaders that should be run of the matched files.

The option property is just arguments passed into the loader. So here, we are telling Babel loader to use the React and Babel preset configuration when transforming the files.

With Webpack v1, the module object would look like this instead:

module: {
  loaders: [
    {
      test: /\.jsx?$/,
      exclude: /node_modules/,
      loader: "babel-loader",
      query: {
        "presets": ["es2015", "react"]
      }
    }
  ]
},

Here, query is the old name for options.

$ ./node_modules/webpack/bin/webpack.js
Hash: 24927170e926bf05766a  
Version: webpack 2.2.0  
Time: 1709ms  
           Asset     Size  Chunks                    Chunk Names
./dist/bundle.js  1.04 MB       0  [emitted]  [big]  app
   [0] ./~/process/browser.js 5.3 kB {0} [built]
   [2] ./~/fbjs/lib/warning.js 2.1 kB {0} [built]
  [15] ./~/react/lib/ReactElement.js 11.2 kB {0} [built]
  [17] ./~/react-dom/index.js 59 bytes {0} [built]
  [18] ./~/react/react.js 56 bytes {0} [built]
  [21] ./~/react/lib/React.js 2.69 kB {0} [built]
  [22] ./~/firebase/firebase-browser.js 259 bytes {0} [built]
  [24] ./~/firebase/app.js 16.4 kB {0} [built]
 [103] ./~/firebase/auth.js 119 kB {0} [built]
 [104] ./~/firebase/database.js 121 kB {0} [built]
 [105] ./~/firebase/messaging.js 16.9 kB {0} [built]
 [106] ./~/firebase/storage.js 29 kB {0} [built]
 [120] ./~/react-dom/lib/ReactDOM.js 5.14 kB {0} [built]
 [177] ./~/react-dom/lib/renderSubtreeIntoContainer.js 422 bytes {0} [built]
 [189] ./src/index.jsx 545 bytes {0} [built]
    + 175 hidden modules

As expected, it built successfully!

Lastly, we need to include the dist/bundle.js into our index.html file.


<body>
  <div id="renderTarget"></div>
  <script src="./dist/bundle.js"></script>
</body>

And run python's SimpleHTTPServer again and see our app in all it's glory! (again!)

$ python -m SimpleHTTPServer 1234

Summary

In this article, we've:

  • Discussed the difference between module loaders and module bundlers
  • Switched from using SystemJS (module loader) to Webpack (module bundler)
  • Learned about three of the four basic concepts of Webpack - entry, output and loaders

Finally, get the complete source code for this article on GitHub!

The dist directory is not included in the repository, as like node_modules, they're supposed to be generated from source.

Daniel Li

Full-stack Web Developer in Hong Kong. Founder of Brew.

Hong Kong http://danyll.com