Setting Up A React Workflow with Babel and Webpack 2

The React team have done an excellent job keeping it up to date, and it has been compatible with some of the new ES2015(or ES6) features since 2015. Sadly, browser support for ES2015 is still incomplete in most of them, but progress has been made. Here's a compatibility table of current desktop browsers.

ES2015 compatibility table

Babel

This is where Babel comes useful. Babel lets us write JavaScript that uses new ES2015 features, and then transforms it into good old ES5 code which runs in most of the browsers and JavaScript environments.

You write:

[1,2,3].map(n => n + 1);

And you get:

[1,2,3].map(function(n) {
  return n + 1;
});

Webpack

Webpack is a module bundler that takes your code and assets such as JavaScript files with a lot of dependencies, CSS/SASS/LESS files, images... and then transforms and bundles them into fewer static assets, so instead of adding a script tag for each React component you are using or every dependency, in your HTML, we can just use one that has them all, or one for all our dependencies and another one for the app. And not only will it bundle it, it will also apply any transformation you want before it creates the final files, like in our case, it will transpile our code from ES2015 to ES5 using Babel, it can also transform your SASS/LESS code into normal CSS, or if you want, it can minify the output files to make them lighter.

Sample Project

We are going to create a very simple React application, to show how these tools work together.

Project Structure

At the end of this tutorial, the project structure will look like this:

├── .babelrc
├── package.json
├── webpack.config.babel.js
├── src
│   ├── index.html
│   ├── index.jsx
│   ├── index.scss
│   ├── components
│   │   ├── app
│   │   │   ├── App.jsx
│   │   │   ├── App.scss
│   │   ├── content
│   │   │   ├── Content.jsx
│   │   │   ├── Content.scss
│   │   ├── footer
│   │   │   ├── Footer.jsx
│   │   │   ├── Footer.scss
│   │   ├── header
│   │   │   ├── Header.jsx
│   │   │   ├── Header.scss

Project Initialization

Make sure you have NodeJS and npm installed.

Let's create a new folder 'react-babel-webpack2' and initialize it with npm, so launch your terminal and go to the directory where you want to save your project:

mkdir react-babel-webpack2  
cd react-babel-webpack2  
npm init --yes  

Then, proceed to install our required dependencies.

React:
npm install react react-dom --save  
npm install react-hot-loader@3.0.0-beta.3 --save  
Webpack:

As of 9/25/2016, webpack 2 is still in beta (v25).

npm install webpack@2.1.0-beta.25 --save-dev  
npm install webpack-dev-server@2.1.0-beta.2 --save-dev  
npm install loader-utils html-webpack-plugin extract-text-webpack-plugin@2.0.0-beta.4 --save-dev  
npm install style-loader css-loader sass-loader node-sass --save-dev  
Babel:
npm install babel-core babel-loader babel-register --save-dev  
npm install babel-preset-es2015 babel-preset-react --save-dev  

React App

Our app will basically be a page, with a header, an input, and a footer. We'll also be using SASS for our components styles, just to show you how webpack can compile these to CSS without you having to do it manually. Really simple, right?

Let's begin making a new folder "src" where we will keep all our app files, and create a simple HTML page that contains a <div> where we will mount our React App:

mkdir src  
cd src  
touch index.html  

src/index.html

<!doctype html>

<html>  
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My React App</title>
  </head>
  <body>
    <div id="main"></div>
  </body>
</html>  

You can see that we didn't include any scripts, no mistake there, webpack is going to take care of that for us. Let's now create the components we specified in the project structure. We'll be using the ES2015 class definition syntax for the stateful components, and normal functions for our pure or stateless components, following Airbnb style guide on this matter.

App Component

This component will contain our header, content and footer, and it's the one we'll mount in <div id="main"></div>.

src/components/App.js

// Dependencies
import React from 'react';

// Components
import Header from '../header/Header.jsx';  
import Content from '../content/Content.jsx';  
import Footer from '../footer/Footer.jsx';

// Styles
import './App.scss';

function App() {  
  return (
    <div className="site-wrapper">
      <Header />
      <Content />
      <Footer />
    </div>
  );
}

export default App;  

src/components/App.scss

.site-wrapper {
    min-height: 100%;
    display: flex;
    flex-direction: column;
}

Header Component

We'll write a pure component for our app header.
src/components/header/Header.jsx

// Dependencies
import React from 'react';

// Styles
import './Header.scss';

function Header() {  
  return (
    <header>
      <h1>Simple React Page</h1>
    </header>
  );
}

export default Header;  

src/components/header/Header.scss

header {  
    width: 100%;
    height: 80px;
    background-color: #6cc72c;
    text-align: center;

    h1 {
        margin: 0;
        text-transform: uppercase;
        font-size: 32px;
        padding-top: 20px;
        font-weight: 400;
        color: #fff;
    }
}

Content Component

This component will have an input and a span to show the text written in it. We have a state with the input value, so we are going to define it using the class syntax.
src/components/content/Content.jsx

// Dependencies
import React from 'react';

// Styles
import './Content.scss';

class Content extends React.Component {  
  constructor(props) {
    super(props);
    this.handleInputChange = this.handleInputChange.bind(this);
    this.state = {
      input: 'World',
    };
  }
  handleInputChange(e) {
    this.setState({ input: e.target.value });
  }
  render() {
    return (
      <div className="content">
        <input
          type="text"
          value={this.state.input}
          onChange={this.handleInputChange}
        />
        <span>Hello {this.state.input}</span>
      </div>
    );
  }
}

export default Content;  

src/components/content/Content.scss

.content {
    background-color: #222;
    flex: 1 0 auto;
    font-size: 40px;
    color: #fff;
    align-items: center;
    display: flex;
    flex-direction: column;
    justify-content: center;

    span {
        max-width: 50%;
    }

    img {
        height: auto;
    }

    input {
        padding: 10px;
        background-color: #2f2f2f;
        border: none;
        font-size: 24px;
        color: #535353;
        text-align: center;

        &:focus {
            outline-color: #1e1e1e;
        }
    }
}

Footer Component

We'll write another simple pure component for our app footer.
src/components/footer/Footer.jsx

// Dependencies
import React from 'react';

// Styles
import './Footer.scss';

function Footer() {  
  return (
    <footer>
      <h1>Just a footer...</h1>
    </footer>
  );
}

export default Footer;  

src/components/footer/Footer.scss

footer {  
    width: 100%;
    height: 100px;
    background-color: #f92672;
    text-align: center;

    h1 {
        margin: 0;
        text-transform: uppercase;
        font-size: 24px;
        padding-top: 20px;
        font-weight: 400;
        color: #fff;
    }
}

To end up, create the index.jsx file which will be the entry point of our React app.
src/index.jsx

// Dependencies
import React from 'react';  
import ReactDOM from 'react-dom';

// Components
import App from './components/app/App.jsx';

// Styles
import './index.scss';

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

src/index.scss

@import url( 'https://fonts.googleapis.com/css?family=Open+Sans');
$font-stack: 'Open Sans', sans-serif;

html {  
    height: 100%;
    font-family: $font-stack;
}

body {  
    height: 100%;
    margin: 0;
    display: flex;
    flex-direction: column;
}

#main {
    height: 100%;
    display: flex;
    flex-direction: column;
}

Setting up Webpack

Up until this point, you haven't been able to preview how is your app looking, so let's begin writing a webpack config that bundles all of our code and injects it into our template HTML.

A config file in webpack is just a node module where you define all the information regarding the build process, which includes the inputs and outputs of your project, any transformation you would like to perform, and more stuff that I'll cover later. This file is typically named webpack.config.js, but we'll be using ES2015 in it as well, so we'll have to name it webpack.config.babel.js. Go ahead and create this file in your root diretory.

import path from 'path';  
import HtmlWebpackPlugin from 'html-webpack-plugin';

export default () => ({  
  entry: [
    path.join(__dirname, 'src/index.jsx'),
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
        filename: 'index.html',
        template: './src/index.html'
    }),
  ]
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        include: path.join(__dirname, 'src'),
        use: [
          {
            loader: 'babel',
            options: {
              babelrc: false,
              presets: [
                ['es2015', { modules: false }],
                'react',
              ],
            }
          }
        ]
      },
      {
        test: /\.(css|scss|sass)$/,
        loader: 'style!css!sass',
      },
    ]
  },
});

As you can see the configuration file exports a function which returns the configuration object. This is new in webpack 2 and I'll explain why it is a good enhancement further down. If you are new to webpack, let me explain what these config keys are for:

entry: This is the entry point of the bundle, and it can either be a single file or multiple files. Webpack will bundle all the files that are required in them, imagine it like a tree. In our case, entry point is src/index.jsx.

output: An object which tells webpack how to write the compiled files to disk. In our case, it will create a bundle.js file in a /dist folder of our root directory.

plugins: They add functionality typically related to the bundle process. Webpack has some built-in plugins, but you can also install third party ones using npm. We are using html-webpack-plugin which will generate an HTML file for you that includes all your webpack bundles in the body using script tags.

new HtmlWebpackPlugin({  
  filename: 'index.html', // Output file name.
  template: './src/index.html' // Use our HTML file as a template for the new one.
}),

rules: This array contains our loaders. Remember how we've said webpack can tranform files, well, these are the ones that allows us to. Loaders will preprocess files as you import/load them. The test and include keys are regular expressions that must be met, test is commonly used to match the file extension while include is used to match directories. exclude is the opposite, a RegEx that must not be met. If conditions are met, it will apply the loaders specified in the use key, we can include multiple loaders by adding loader objects to the use key or using webpack 1 way by having a loader string chaining names of the loaders together separated with a ! like in our styles rule. Loaders can also accept options, which will could pass via an object in the option key, like in our JS ans JSX babel loader.

Now, we need to create a .babelrc file in the root directory that contains the babel options that will be used to transpile our webpack config file.

.babelrc

{
    "presets": [
        "es2015"
    ]
}

Make sure you have installed babel-register module, because webpack.config.babel.js transpilation depends on it.

You can see that we included a different configuration for our babel loader, although we are able to use the .babelrc file with webpack, we included another configuration in our webpack config shown below.

query: {  
  babelrc: false, // Tells webpack not to use the .babelrc file.
  presets: [
    ['es2015', { modules: false }],
    'react', // Strip flow types and transform JSX into React.createElement calls.
  ],
}

Webpack 2 brings native support for ES2015 Modules. It now understands import and export without them being transformed to CommonJS requires. In our presets, we include ['es2015', { modules: false }], which disables transformation of the ES2015 module syntax, and that's why we use different babel configurations, because webpack config uses import which isn't supported by node yet.

You are now finally ready to see your app, create a npm script in your package.json as shown below:

{
  ...
  "scripts": {
    "webpack": "webpack"
  }
  ...
}

Go ahead and run npm run webpack in your console. If everything goes well, you should get something like this:

Hash: cd87a0cbb454963b1c53  
Version: webpack 2.1.0-beta.22  
Time: 3052ms  
     Asset       Size  Chunks             Chunk Names
 bundle.js     747 kB       0  [emitted]  main
index.html  302 bytes          [emitted]  
 [188] multi main 28 bytes {0} [built]
    + 188 hidden modules
Child html-webpack-plugin for "index.html":  
        + 4 hidden modules

Go to the /dist folder and open index.html to see your app.

See the Pen React App by Daniel Reinoso (@danielr18) on CodePen.

Hot Module Replacement

This is an awesome feature for development, which exchanges, adds, or removes modules (i.e. a react component) while an application is running without a page reload. That means you can edit any JS module or CSS stylesheet and as soon as you save them, you'll see the changes reflected in your browser without needing to refresh.

To make this work, let's write the following changes to our webpack config:

// ...
import {HotModuleReplacementPlugin} from 'webpack';

export default () => ({  
  entry: [
    'react-hot-loader/patch', // Needed to preserve state
    'webpack-dev-server/client?http://localhost:8080', // webpack dev server host and port
    // ...
  ],
  // ...
  plugins: [
    new HotModuleReplacementPlugin(), // Globally enable hot code replacement
    // ...
  ],
  module: {
    rules: [
      {
        test: /.jsx?$/,
        // ...
        options: {
          // ...
          plugins: ['react-hot-loader/babel'],
        }
        // ...
      },
      // ...
  },
  devServer: {
    hot: true,
  },
});

A module can only be updated if you "accept" it. So you need to module.hot.accept the module in the parents or the parents of the parents. Let's accept our App component:

import React from 'react';  
import ReactDOM from 'react-dom';  
import { AppContainer } from 'react-hot-loader'; // required

import App from './components/app/App.jsx';

import './index.scss';

function renderApp() {  
  // We now render `<AppContainer>` instead of our App component. 
  ReactDOM.render(
    <AppContainer>
      <App />
    </AppContainer>,
    document.getElementById('main')
  );
}

renderApp(); // Renders App on init

if (module.hot) {  
  // Renders App every time a change in code happens.
  module.hot.accept('./components/app/App.jsx', renderApp);
}

Let's create another npm script to run webpack-dev-server:

package.json

{
  ...
  "scripts": {
    "dev": "webpack-dev-server"
  }
  ...
}

Go ahead and run it. Have some fun editing your components or styles and see how they instantly update.

Dev / Production Environment

We usually have different settings for each environment, we don't need HMR when we are not in development env, or we would like to save all our styles in a css file when we are in production. Back in webpack 1, people used to have different files for each environment, and depending on the NODE_ENV variable it loaded or merged the environment configs to a base config.

Remember how I said that webpack 2 now exports a function that returns a config object, and that it was a good thing, well this is why, this function accepts a string or an object which is passed by the CLI via --env (i.e. passing --env.minimize --env.server localhostin the CLI is transformed to {minimize: true, server: "localhost"}).

So now our webpack config can have other entries, loaders, plugins... depending on whatever is passed to the function. In our case, we'll have a simple object with 2 booleans dev and production. Let's set up a default env for our function, as well as our adapted npm scripts:

webpack.config.babel.js

// ...
const defaultEnv = {  
    dev: true,
    production: false,
};

export default (env = defaultEnv) => ({  
    // ...
});

package.json

{
  ...
  "scripts": {
    "dev": "webpack-dev-server --env.dev",
    "production": "webpack -p --env.production",
  },
  ...
}

We will use the following syntaxis for our config:

// Spread opearator (...)
console.log([1, 2, 3]); // [1, 2, 3]  
console.log(...[1, 2, 3]); // 1 2 3

// Ternary and spread operator to create arrays

const arr = [  
  ...bool ? [1,2] : [3, 4] 
]

// If bool is true, arr will be [1, 2].
// If bool is false, arr will be [3, 4].

Most of the config keys are arrays like plugins, rules, presets... so we'll use the spread and ternary operators to create these arrays, depending on the environment variables. Our new config will look like this:

import path from 'path';  
import HtmlWebpackPlugin from 'html-webpack-plugin';  
import {HotModuleReplacementPlugin} from 'webpack';

const defaultEnv = {  
    dev: true,
    production: false,
};

export default (env = defaultEnv) => ({  
  entry: [
    ...env.dev ? [
      'react-hot-loader/patch',
      'webpack-dev-server/client?http://localhost:8080',
    ] : [],
    path.join(__dirname, 'src/index.jsx'),
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  plugins: [
    ...env.dev ? [
      // Webpack Development Plugins
      new HotModuleReplacementPlugin(),
    ] : [],
    new HtmlWebpackPlugin({
        filename: 'index.html',
        template: path.join(__dirname, 'src/index.html'),
    }),
  ],
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        include: path.join(__dirname, 'src'),
        use: [
          {
            loader: 'babel',
            options: {
              babelrc: false,
              presets: [
                ['es2015', { modules: false }],
                'react',
              ],
              plugins: ['react-hot-loader/babel']
            }
          }
        ]
      },
      {
        test: /\.(css|scss|sass)$/,
        loader: 'style!css!sass',
      },
    ]
  },
  devServer: {
    hot: env.dev
  },

});

Now when you run npm run dev it will use webpack-dev-server and HMR will be enabled, and if you run npm run production it will only create our files and put them in /dist. We also included a new key devtool and will generate source maps for our minified code. You can read more about why we use different options to produce source maps depending on the environment here.

You have probably noticed already that webpack isn't creating any css file, but we can see that styles are applied anyway, this is because stylesheets are being embedded in our JS bundle. Let's configure webpack to create a css file when we run in production.

We are going to use extract-text-webpack-plugin for this:

// ...
import ExtractTextPlugin from 'extract-text-webpack-plugin';

export default (env = defaultEnv) => ({  
  // ...
  plugins: [
    ...env.dev ? [
      // ...
    ] : [
      // Webpack Production Plugins
      new ExtractTextPlugin('[name].css'),
    ],
    // ...
  ],
  module: {
    rules: [
      // ...
      {
        test: /\.(css|scss|sass)$/,
        loader: env.dev ? 'style!css!sass' : ExtractTextPlugin.extract({
          fallbackLoader: 'style',
          loader: 'css!sass'
        })
      },
    ]
  }
  // ...
});

Run npm production now, and you'll see a new main.css file in our /dist folder.

You have come to the end of this tutorial, you can now use this as a base for new apps you would like to create, and add more functionality to your webpack config. You can take a look at the final code here. See you in the next tutorial.