Learning Webpack with React and ES6

When I create a React/ES6 app, I reach for my tool of choice – browserify. However, that has certain issues – things that I am certain I can work around if I try, but I’m spending all my time in the tooling. I want to write the application – not the tools. A lot of my articles over the past year have been simply adjustments to my tooling. It’s time for a different approach. Way back in the early parts of last year, I mentioned Webpack in the same breath as Browserify. Webpack is a different style of bundler. It bundles everything together – not just your code. To get to the point of using Webpack, I need some configuration. My hope is that I can build the entire web site with a single build script.

Firstly, let’s get rid of Gulp. That means getting rid of the Gulpfile.js and my gulp directories. It also means I need to handle linting and testing elsewhere. I’ve done this by changing the scripts section of my package.json to the following:

  "scripts": {
    "clean": "rimraf public",
    "pretest": "eslint client/src server/src client/test server/test && sass-lint -v -q -f stylish",
    "test": "mocha --recursive --compilers js:babel-register --reporter dot client/test server/test",
    "start": "node ./bin/www"
  },

Of these, only the start script was there before. I can run npm test to test the package. I can also run npm run clean to clean up the generated files. This handles all the tasks except for the building of the public area. My next step is to integrate the index.html file to the express server. Right now, I serve it up as a static file, and the gulp build system would copy the index.html file from the source area to the destination area. I’ve added the following to my server (the code goes above the staticFiles() middleware initialization):

    // Display the index.html file
    app.get('/', function (request, response) {
        response.status(200).type('text/html').send(index);
    });

The index variable is initialized to the content I want to send elsewhere. The variable is just a copy of my old index.html file.

Webpack Basics

On to the webpack configuration. Webpack, at it’s core, bundles things together and then allows you to load them via loaders. In order to load my JSX files, I need to include the Babel transpiler. The idea is that the files will be compiled from ES6 to normal browser-ready JavaScript. Babel also compiles my JSX for me, so I don’t need an extra step for that. Just like Gulp and Grunt before them, Webpack has a configuration file: webpack.config.js – it’s a Javascript file that exports the configuration object. Here is my simple version:

module.exports = {
    entry: {
        grumpywizards: './client/src/app.jsx'
    },
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                loaders: [ 'babel' ],
                exclude: /node_modules/
            }
        ]
    }
    output: {
        filename: 'public/[name].js'
    }
};

This will walk the entry point, compile all the source files into the right form and then store the file in public/grumpywizards.js. The module.loaders has three elements – firstly, a test – this is a regular expression that is matched against the filename. If the test matches, this loader definition is used. The version here accepts .js and .jsx extensions. The next thing is the exclude – this says don’t include anything in node_modules. Finally, the list of loaders is applied from right to left – I’ve only got one – babel – because Babel compiles JSX as well.

To run this, you need to install webpack and the loader:

npm install --save-dev webpack babel-loader

You can now add the following script definition to the package.json:

  "scripts": {
    "clean": "rimraf public",
    "pretest": "eslint client/src server/src client/test server/test && sass-lint -v -q -f stylish",
    "test": "mocha --recursive --compilers js:babel-register --reporter dot client/test server/test",
    "prestart": "webpack -p",
    "start": "node ./bin/www"
  },

When you run npm start now, the webpack is run prior to starting the server. What you will see is an uglified packed file. Note that it’s on the larger side – about 1MB. That’s because all the React libraries are included (and after I just got rid of them with Browserify!). Also, there are no source maps yet.

Dealing with External Libraries

In my last article, I used an extra module – browserify-shim – to abstract libraries from my code. This functionality is built in to webpack. I just need to add a little bit of configuration to the webpack.config.js:

module.exports = {
    entry: {
        grumpywizards: './client/src/app.jsx'
    },
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                loaders: [ 'babel' ],
                exclude: /node_modules/
            }
        ]
    },
    externals: {
        'react': 'React',
        'react-dom': 'ReactDOM'
    },
    output: {
        filename: 'public/[name].js'
    }
};

On the left hand side is what the module is called. On the right hand side is the global variable that it is exposed as when you load the library from the CDN. Yep – this is super simple. Building this takes my library from 1MB to 2Kb, which is eminently more reasonable. I’ll leave the library serving to the CDN.

Source Maps

Just like the external library configuration, source maps has been thought about as well. Just add an option to generate source maps to your webpack.config.js:

module.exports = {
    entry: {
        grumpywizards: './client/src/app.jsx'
    },
    devtool: 'source-map',
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                loaders: [ 'babel' ],
                exclude: /node_modules/
            }
        ]
    },
    externals: {
        'react': 'React',
        'react-dom': 'ReactDOM'
    },
    output: {
        filename: 'public/[name].js'
    }
};

The important line here is the devtool configuration – there are various values here but the source-map value should make the webpack emit a .map file.

I had a major amount of pain here though. Most of the online tutorials suggested putting babel-loader and jsx-loader in. This caused the source maps to be the ES5 versions of the files – after transpiling. However, Babel transpiles JSX as well, so there is no need for jsx-loader. Well, it turns out that jsx-loader doesn’t have source map support. Fortunately, we don’t need jsx-loader any more. So just do away with it and be happy.

Wrap Up

So, where do we go from here. I like webpack – much more than the gulp + browserify + babelify + all the rest. There is still some work to do. I need to find a solution to stylesheets for my components and I want to start looking at live reloading as I save files. That, however, is for another day. In the mean time, you can find my continuing code at my GitHub Repository.