Alan Shortis

Webpack and Babel for React

There so many ways to bootstrap a React app without touching any config. Let's put together a sensible config from scratch in order to better understand what's going on under the hood.

Index

  1. Assumptions
  2. Create the project
  3. Babel
  4. Webpack
  5. React
  6. Development Server
  7. Styled Components
  8. Code Quality
  9. Production Build

There are various ways to write a React app without the need to deal with tooling and configuration. Create React App, Next.js and Gatsby are all excellent and can be used to make a complete, finished app (depending on your needs).

This guide will explain how to configure a React application from scratch. You may need to do this to meet the needs of your project, or you might just be curious to learn how your React source ends up in a format that can be understood by browsers.

Assumptions

This guide assumes a few things that will not be covered:

  • You have Node.js and NPM installed. At the time of writing, the latest versions are 11.6.0 and 6.5.0 respectively.
  • You know enough JavaScript and React to get by. We won't be touching React much, but what little we do write will not be explained.
  • You're comfortable enough on the command line to navigate the file system and run commands.

Create project

Create a new folder for your project. Let's call it react-project for now.

We'll be using NPM to handle all of our dependencies, so we need to initialise it and create a package.json. Run $ npm init -y (the -y here will accept all default values for each property, which is fine to get started).

If you're going to be using git, now is a good time to ignore some files. Create a .gitignore file and add a few entires:

node_modules
npm-debug.log
dist

Babel

Babel is a toolchain that will take your modern JavaScript source and transpile it down to a version that can be understood by browsers. For example, IE11 cannot work with JavaScript classes so Babel will turn them back in to plain old Prototypes.

The Babel documentation is very good and explains everything in much more detail.

Install Babel, presets to handle modern JavaScript and React, and a Babel loader (this is for Webpack, which will be explained later): $ npm i --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader.

The Babel presets add support for transpiling particular parts of JavaScript. preset-env will take care of modern (think ES6 and beyond) features and preset-react will take care of features specific to React (such as JSX).

Create a config file for Babel named .babelrc and add the presets we just downloaded:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

Webpack

At its heart, Webpack is a module bundler. It takes all the pieces of JavaScript we're using and puts them together into a bundle that can be used by the browser.

Part of the process of creating bundles is running Babel, but it can automate many other tasks too. In this case, Webpack will:

  • Take our JavaScript modules (React, our own components, etc), transpile them with Babel and save them all in a new bundle.
  • Copy files between our source and destination folders at build time.
  • Start a development server that will automatically refresh as we work.
  • Create an optimised build ready for production.

Install Webpack and the Webpack CLI as dev dependencies: $ npm i --save-dev webpack webpack-cli.

Create a new webpack config named webpack.config.js and add the below configuration to get started:

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
    ],
  },
};

With this configuration, running Webpack will look for all files with a .js or .jsx extension (excluding those that have been installed from npm) and run the Babel loader against them. The end result will be a transpiled JavaScript bundle comprised of each of our modules.

To define what browsers we want to support, add a new property in package.json whose value describes the target:

"browserslist": ["last 2 versions"]

Webpack uses browserslist for this, and details on how to specify the browsers you're interested in can be found in their documentation. For now, the last 2 versions of each browser will do.

We need an HTML element for React to render to, so let's create one and add to our Webpack config so the file is copied from our source folder to the destination folder.

Install the html loader and the html webpack plugin: $ npm i --save-dev html-webpack-plugin html-loader.

Create a new folder named src for our source files at the root of the project and create a new HTML file named index.html. Add the below markup, noting the div with an ID of root.

<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

TIP: If you're using emmet you can create this very quickly using: html:5>#root.

Add to your webpack config to use the HTML loader:

const HtmlWebPackPlugin = require('html-webpack-plugin');
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {        test: /\.html$/,        use: {          loader: 'html-loader',        },      },    ],
  },
  plugins: [    new HtmlWebPackPlugin({      template: './src/index.html',      filename: './index.html',    }),  ],};

React

Now we have some tooling taken care of, we can install React and create the most basic App.

Install React and ReactDOM as dependencies: $ npm i --save react react-dom.

In the src folder, create a new JavaScript file for our React App named index.js.

import React from 'react';
import { render } from 'react-dom';

const App = () => <span>This is my React app.</span>;

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

That's it for now - we're just directly rendering one stateless functional component to the div with an ID of 'root' that we added to our HTML file earlier.

Development Server

We've yet to see anything in a browser yet. In order to do that we're going to configure Webpack to use Webpack Dev Server and Browsersync together.

The Webpack Dev Server is very quick and easy to set up and provides excellent features such as Hot Module Replacement. Browsersync allows you to use multiple browsers and devices at the same time, with scrolling and clicking synced everywhere.

Install the Webpack Development Server and Browsersync: $ npm i --save-dev webpack-dev-server browser-sync browser-sync-webpack-plugin, and add the configuration to webpack.config.js.

We're also adding devtool and devserver properties which:

  • devtool ensures that sourcemaps are created and available when debugging in the browser.
  • devserver ensures that any 404 errors fallback to index.html, which is essential if using something like React Router for client side routing.
const HtmlWebPackPlugin = require('html-webpack-plugin');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.html$/,
        use: {
          loader: 'html-loader',
        },
      },
    ],
  },
  plugins: [
    new HtmlWebPackPlugin({
      template: './src/index.html',
      filename: './index.html',
    }),
    new BrowserSyncPlugin(      {        host: 'localhost',        port: 3000,        proxy: 'http://localhost:8080/',      },      {        reload: false,      }    ),  ],
  devServer: {    historyApiFallback: true,  },  devtool: 'inline-source-map',};

This config is:

  • Telling Browsersync to run on port 3000 on localhost.
  • Telling BS to proxy port 8080, which is where WDS is running. By doing this, we get the features of both tools at the minor expense of using two ports on localhost.
  • Setting reload to false stops Browsersync from reloading as we save - WDS has that covered.

In package.json, we can create a new script entry for starting this development server (you can remove the test script already defined):

"scripts": {
  "start": "webpack-dev-server --mode development --port 8080 --hot"
},

Running $ npm start will bundle our code, write sourcemaps, start the dev server in development mode with HMR, start Browsersync and open your browser on the correct url and port. You'll also get a summary in your terminal:

--------------------------------------
      Local: http://localhost:3000
  External: http://10.216.69.213:3000
--------------------------------------
        UI: http://localhost:3001
UI External: http://localhost:3001
--------------------------------------

The External URL can be used by any device on the same network to view your Dev Server. The UI URL is a dashboard for Browsersync where you can adjust some settings.

Visiting http://localhost:3000 will display your amazing React App.

Tip: React Developer Tools is an essential extension available for Chrome and Firefox that makes debugging React much easier, as well as looking under the hood of third party apps built in React.

Styled Components

Styled Components is a CSS-in-JS solution that takes advantage of tagged template literals. You can write CSS in a style really close to vanilla CSS, with the advantage of using JavaScript to augment it.

Install styled components as a dependency: $ npm i --save styled-components.

There is also a Babel plugin that provides better debugging: $ npm i --save-dev babel-plugin-styled-components. Add this plugin to .babelrc:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": ["babel-plugin-styled-components"]}

We can now use Styled Components in our existing React app:

import React from 'react';
import { render } from 'react-dom';
import styled from 'styled-components';const StyledText = styled.span`  color: red;`;const App = () => <StyledText>This is my React app.</StyledText>;
render(<App />, document.getElementById('root'));

Code Quality

Prettier and ESLint

Prettier and ESLint are tools that help ensure that your code adheres to standards and is correctly formatted. There is a little bit of overlap in what these tools do, so by configuring them together we can get the benefits of both.

We're going to use the popular Airbnb ESLint config to set some sensible defaults, including a config and plugin that takes prettier into account.

The Airbnb config expects various peer dependencies, and these can all be installed together: $ npx install-peerdeps --dev eslint-config-airbnb.

We also need to install the babel parser for ESLint, Prettier and the Prettier configs and plugins so it'll work nicely with ESLint: $ npm i --save-dev babel-eslint prettier eslint-config-prettier eslint-plugin-prettier.

Create a .eslintrc config file, extending each config, using the prettier plugin and using the babel-eslint parser:

{
  "extends": ["airbnb", "prettier", "prettier/react"],
  "plugins": ["prettier"],
  "globals": {
    "document": true
  },
  "rules": {
    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }]
  },
  "parser": "babel-eslint"
}

We're adjusting the Airbnb rules here slightly. The rules state that all files that contain JSX must use a .jsx file extension. Some people prefer to just use a .js extension, and this rule will allow either.

To use .jsx files, we'll need to add a property to our webpack config that will ensure JSX files are resolved:

resolve: {
  extensions: ['.js', '.jsx'],
},

With this added, you could go back and rename src/index.js to src/index.jsx if you prefer.

Create a .prettierrc config with some personal preferences. The default config for Prettier is really sensible and well thought out, so if you like it you only need to include an empty object.

{
  "singleQuote": true,
  "trailingComma": "es5",
  "printWidth": 100
}

There are a few rules I like to override:

  • Set quotes to single, as the default is double.
  • Ensure trailing commas are used. I like this for source control, as it means a line won't appear to have been authored by a developer who only added a comma at the end.
  • Set a longer line length.

Stylelint

Like ESLint for JavaScript, stylelint enforces rules for CSS. Install stylelint, a processor for styled components (so we can use this with Styled Components rather than just regular CSS or SASS) and the recommended/styled components configs: $ npm i --save-dev stylelint stylelint-processor-styled-components stylelint-config-styled-components stylelint-config-recommended.

Create a .stylelintrc with this config, which extends the configs we just downloaded. I have also added some rules I like, but you should take the time to read the available rules as I find the recommended rules a bit too lenient.

{
  "processors": ["stylelint-processor-styled-components"],
  "extends": ["stylelint-config-recommended", "stylelint-config-styled-components"],
  "rules": {
    "block-closing-brace-newline-after": "always",
    "declaration-block-trailing-semicolon": "always",
    "declaration-no-important": true,
    "declaration-colon-space-after": "always",
    "color-named": "never",
    "selector-max-id": 0,
    "selector-max-universal": 0,
    "number-leading-zero": "never",
    "number-no-trailing-zeros": true,
    "length-zero-no-unit": true,
    "unit-case": "lower"
  }
}

If using Visual Studio Code, the Prettier, ESLint and stylelint plugins will add features to the editor. Issues will be underlined in code, and Prettier rules will be applied on save so you never need to think about things like indentation.

To enable the format on save feature, add to your VS Code settings.json:

"editor.formatOnSave": true,
"prettier.requireConfig": true,

The prettier.requireConfig setting will prevent VS Code from applying prettier formats on save in projects that do not have a prettier config. This is especially useful if you're in an older codebase with established formatting that deviates from Prettier.

Linting

We now have linting rules for both JavaScript and CSS, so we should add methods to use them. First of all, we'll create scripts to run just the linters (which could be helpful for CI/CD workflows).

Add scripts to package.json:

"scripts": {
  "start": "webpack-dev-server --mode development --port 8080 --hot",
  "lint:js": "eslint --ext .js --ext .jsx src",  "lint:css": "stylelint src/**/\*.js src/**/\*.jsx"}

Each of these scripts will look inside our source folder and sub folders, and check that the linting rules pass for all js and jsx files.

When running the CSS linter, you'll notice it flags an issue. We have used the red colour keyword in our Styled Component which our rules forbid. To solve this, go back to src/index.js and set the colour in hex:

import React from 'react';
import { render } from 'react-dom';
import styled from 'styled-components';

const StyledText = styled.span`
  color: #ff0000;`;

const App = () => <StyledText>This is my React app.</StyledText>;

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

Rerunning the linter should now show no output at all, as all rules have been adhered to.

Linting on save with Webpack

Webpack can be configured to run the linters as we work, as part of our existing development server configuration.

First, we need eslint and stylelint loaders: $ npm i --save-dev eslint-loader stylelint-custom-processor-loader, and add to our existing rule for js/jsx files:

const HtmlWebPackPlugin = require('html-webpack-plugin');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ['babel-loader', 'eslint-loader', 'stylelint-custom-processor-loader'],      },
      {
        test: /\.html$/,
        use: {
          loader: 'html-loader',
        },
      },
    ],
  },
  plugins: [
    new HtmlWebPackPlugin({
      template: './src/index.html',
      filename: './index.html',
    }),
    new BrowserSyncPlugin(
      {
        host: 'localhost',
        port: 3000,
        proxy: 'http://localhost:8080/',
      },
      {
        reload: false,
      }
    ),
  ],
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};

This extends the existing rule for js and jsx files, and adds the ESLint and stylelint loaders. The order of the loaders in the use array is important - we need to lint our source files before Babel touches them, so ensure each linting loader is after the Babel loader in the array.

Production Build

Webpack can handle a production ready build for us, creating an uglified JavaScript bundle free of code used only for development/debugging. This is achieved using the mode parameter we already used for our development server.

Add a new build script to package.json:

"scripts": {
  "start": "webpack-dev-server --mode development --port 8080 --hot",
  "lint:js": "eslint --ext .js --ext .jsx src",
  "lint:css": "stylelint src/**/\*.js src/**/\*.jsx",
  "build": "webpack --mode production --progress"}

The --progress option here will print the build progress to the console when run. It's not essential, it just provides a little more information which can be helpful if your build is large.

Running npm run build will create a new dist folder containing our bundles, and will copy the HTML file we created earlier.

We can add some optimisation to the bundle in our Webpack config. Here, we're going to strip all comments which the built in build doesn't do. Install the uglify JS plugin as a dev dependency: $ npm i --save-dev uglifyjs-webpack-plugin, and add to our Webpack config.

This will be our final Webpack config.

const HtmlWebPackPlugin = require('html-webpack-plugin');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ['babel-loader', 'eslint-loader', 'stylelint-custom-processor-loader'],
      },
      {
        test: /\.html$/,
        use: {
          loader: 'html-loader',
        },
      },
    ],
  },
  plugins: [
    new HtmlWebPackPlugin({
      template: './src/index.html',
      filename: './index.html',
    }),
    new BrowserSyncPlugin(
      {
        host: 'localhost',
        port: 3000,
        proxy: 'http://localhost:8080/',
      },
      {
        reload: false,
      }
    ),
  ],
  resolve: {
    extensions: ['.js', '.jsx'],
  },
  optimization: {    minimizer: [      new UglifyJsPlugin({        uglifyOptions: {          output: {            comments: false,          },        },      }),    ],  },};

We should now have a marginally smaller bundle. Not by much, but it all helps.

Conclusion

This should be a good start for understanding how the tooling works, but is the tip of the iceberg for Webpack configuration for React. As you build a large and complex application you'll likely need to refine this config by adding more Babel plugins to handle emerging JavaScript features, add loaders for different file types and strengthen linter rules to suit your team.