Let's build a boilerplate with React and Webpack!

Written by samuelomole | Published 2019/05/30
Tech Story Tags: react | web-development | javascript | webpack | programming

TLDRvia the TL;DR App

In a previous article, we were able to go through a brief history of webpack and also build a simple to-do application and bundle it up with Webpack without any custom Webpack configuration. In this article, we will be building a ReactJS boilerplate.

The reason for choosing a boilerplate is so that we can write our own custom webpack configuration and in the process understand all the most important aspects when it comes to using Webpack in your applications. We will not just build a basic configuration but we will have a production-ready Webpack configuration at the end of this article that can be used as a starting point for your React applications.

Here’s what the final boilerplate looks like (creative right?)

First things first, we need to understand the structure of a typical webpack configuration and get a good understanding of some of the main building blocks of webpack configurations. Below is a typical webpack config:

module.exports = {
  entry: "",
  output: {
  },
  devServer: {
  },
  devtool: debug ? "cheap-module-eval-source-map" : false,
  resolve: {
  },
  module: {
    rules: [
    ]
  },
  plugins: [],
  optimization: {

}
};

First, every webpack config must either be an object or a function that returns an object, in our case here it’s an object. Inside that object, we have a few options that are used in or config. Let us go through each one to understand their functionality:

  • entry — This option accepts the name of a file as its property, webpack uses to file to start bundling (it will be at the top of the dependency graph). It is the entry point to the entire project and we can have more than one. This key accepts a string, object or an array and it defaults to “src/index.js” if it isn’t specified in the config.

  • output — This option is an object that contains several options that determine how webpack emits results. Basically, the options tell webpack where the bundled files can be kept, the format by which to name the bundles e.t.c. Check out the webpack docs for a more detailed explanation.

  • devServer — This is mainly for development, the options that are passed into this object are used to affect/modify the behavior of webpack-dev-server.

  • devtool — This option is used to determine whether or not we want source maps for our code and if we do then what kind of source mapping do we want (if you don’t know much about source maps then check out this awesome article on it).

  • resolve — This is one of my favorite options in webpack. If you’ve worked in a Vue project and you’ve come across import Component from @/components/Component , it is this property that handles it. It controls how file paths, extensions e.t.c are resolved in your codebase, for example, when we set .js files to be automatically resolved, then we can do something like this:

    import something from 'somefile'

    //instead of

    import something from 'somefile.js'

We are going to be using this in our boilerplate.

  • module — This option is used to configure how different types of module are treated in a project, this is a key configuration in our project as it's used to handle, load any file type in our project, from JS to images to our styles. To see a more detailed explanation of this property read the webpack docs.
  • plugins — According to Webpack maintainer, Sean Larkin, “Everything is a plugin in webpack”. This option is used to customize the webpack build process and add our own external plugins. For example, during the building process, we can tell webpack to extract all our stylesheet to separate files using an external plugin (which we will do shortly).
  • optimization —By default, since webpack 4, webpack helps you run optimizations depending on the chosen mode (development or production), but you can override them manually, which we will be doing in our case.

Getting Started

Now that we understand the structure of a typical webpack configuration, let us start building our boilerplate. To follow along you will need:

  • NodeJS (version ≥ 8)
  • A text editor

If you just want to see the code then visit the repository.

Let’s start off by creating an empty npm project, while this is not necessary (we can manually create a package.json file in a directory), it seems right that we follow that convention as it automatically creates the package.json file for us and prepopulates it.

To initialize the npm project, create a folder named bare-react-ap , navigate into that folder and then run the npm init -y command:

mkdir bare-react-app

cd bare-react-app

npm init -y # this command automatically accepts 'yes' for all questions that will be asked during initialization

With that done we should have a project with a package.json file that looks like this:

{
  "name": "bare-react-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

We are going to be adding a lot of devDependencies so, let’s install the first set of dependencies and then we will go through what each one does:

Enter the command below in the bare-react-app folder:

npm install --save-dev @babel/cli @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-proposal-object-rest-spread @babel/plugin-syntax-dynamic-import @babel/preset-env @babel/preset-react babel-loader

Yes, that’s a whole lot of dependencies, but trust me, they are all achieving a common goal:

  • @babel/cli — this is the babel command line interface that allows us to compile files from the command line (if you do not know what babel is used for, please check out their site)

  • @babel/core — this is the core babel compiler

  • @babel/plugin-proposal-class-properties — this is a babel plugin that allows us to use features which don’t exist in Javascript yet (but is proposed), it enables us to use static class properties and property initializer syntax in our JS code, for example:

    class Animal extends React.Component{ // static class properties static name = "new name";

    // property initializer syntax state = { firstName: "", lastName: "" } }

  • @babel/plugin-proposal-decorators — allows us to use @Decorator syntax in our code.

  • @babel/plugin-proposal-object-rest-spread — this plugin allows us to use the rest and spread properties and handles the compilation to ES5.

  • @babel/plugin-syntax-dynamic-import — allows parsing of the import() statement

  • @babel/preset-env — this is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms are needed by your target environment. This particular preset is really extensible but we will focus on using the default.

  • @babel/preset-react — Basically, it allows us to write React, it handles the compilation of JSX.

  • babel-loader — This will be used in our webpack configuration, to process JS and JSX files.

For our next batch of devDependencies , we are going to install packages that will be used in our webpack configuration, install the following:

npm install --save-dev circular-dependency-plugin compression-webpack-plugin copy-webpack-plugin css-loader file-loader html-webpack-plugin mini-css-extract-plugin node-sass sass-loader style-loader terser-webpack-plugin url-loader webpack webpack-bundle-analyzer webpack-cli webpack-dev-server

The most important dependencies to note are, webpack, webpack-cli and webpack-dev-server. I will not explain this here as I will be explaining what each one does during the configuration.

Now let’s add the only 2 production dependencies, which are, React and React Dom. Enter the following command:

npm install react react-dom

Let's now add a few files that we need for our boilerplate.

Create a new folder in the root of our project (where the package.json file is located) and name it src . Then create an index.html file which is our only HTML file. Copy the code below into the index.html file:

<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="A simple React boilerplate">
  <title>Bare React App</title>
 </head>

<body>
   <div id="app"></div>
  <noscript>
            <p>You need Javascript enabled for this application to work</p>
  </noscript>
 </body>
</html>

Then, create an index.js file which will be the entry point for webpack in our application. Paste the following code into it:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App.js";
ReactDOM.render(<App />, document.getElementById("app"));

Now that we have that, create an App.js file, this is where our React component will be located, paste the code below into it:

import React, { Component } from "react";
import logo from "./logo.svg";
import "./App.css";

class App extends Component {

render() {
    return (
      <div className="App">
        <header className="App-header">WELCOME TO NOTHING 🤗</header>
        <img className="App-logo" src={logo} />
      </div>
    );
  }
}

export default App;

We will be creating the CSS file next, but for the logo.svg you can get that from the repo for this project.

If you do not need the logo.svg you can simply remove it from the import and then remove the image tag. If you did that then your App.js file should look like this:

import React, { Component } from "react";
import "./App.css";
class App extends Component {
render() {
    return (
      <div className="App">
        <header className="App-header">WELCOME TO NOTHING 🤗</header>
      </div>
    );
  }
}
export default App;

Next, we create a CSS file named App.css inside the src folder and paste the following style into it:

*{
  padding: 0;
  margin: 0;
}
.App {
  background-color: #282c34;
  text-align: center;
}

.App-logo {
  animation: App-logo-spin infinite 20s linear;
  height: 40vmin;
  pointer-events: none;
}

.App-header {
  height: 70px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(30px + 2vmin);
  color: white;
  padding: 20px;
  cursor: pointer;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

To be able to run this app we need to run a webpack command to bundle up all these files and then serve them to the browser. There are some things to note though, when we are using the app in development we’ll be using webpack-dev-server which according to the webpack docs:

“The dev server uses Webpack’s watch mode. It also prevents webpack from emitting the resulting files to disk. Instead, it keeps and serves the resulting files from memory.” — This means that you will not see the webpack-dev-server build in bundle.js, to see and run the build, you must still run the webpack command.

When using webpack-dev-server we get the benefit of hot-reloading (since it basically uses webpack in watch mode so that it listens for changes in our file).

To use webpack-dev-server in our app, a script will be added in our package.json file:

...

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --mode development --hot"
  },

...

I explained what mode is in a previous article on webpack, but what this script does is to start up webpack-dev-server in development mode with hot reloading enabled. To run the command, in your console type:

npm run dev

You should get an error, and its mainly because webpack does not understand the jsx syntax that we are using, and that is where our custom webpack configuration comes in.

In the root of your project, where your package.json file is located, create a new file called webpack.config.js . When any webpack command is run, it checks to see if there is any file with that name before running its default configuration. Now in the file, we need to import some packages that were installed earlier because we need them now:

const debug = process.env.NODE_ENV !== "production";
const webpack = require("webpack");
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CircularDependencyPlugin = require("circular-dependency-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");

You may have noticed that we now use require statements for imports in this file, that is because webpack uses Node (nodeJS) for the bundling and previous versions of NodeJS do not support ES modules (even though Node v10 and v12 support it under the flag experimental-modules but it is for .mjs), that's why we have to use require which is what is used to import files in nodeJS.

Next, we export an empty object which will contain all our configurations, remember I earlier mentioned that the config is either an object or a function that returns an object:

...
module.exports = {

}

The first configuration that will be added is our entry point, which tells webpack where it should start executing, it can take several formats but I usually stick to the string and object. In the object we created above, add the following code:

...
  entry: "./src/index.js",
...

This tells webpack that the entry point of our application starts here (webpack begins to build its dependency graph from here).

The next configuration is our “output”, as the name implies, it tells webpack how to output the bundled JS. It is an object that takes several options but we will focus on a few, add this code below to the object in our config file:

output: {
    publicPath: "/",
    path: path.join(__dirname, "build"),
    filename: "js/[name].bundle.min.js",
    chunkFilename: "js/[name].bundle.js"
 },

We are telling webpack that it should serve all files from the / path in the browser and also the naming convention for any generated bundle.

Our next configuration is the devServer which is used to configure webpack-dev-server and describes where the base directory of the project is and also what port should the application be served from. Add the code below:

...
devServer: {
    inline: true,
    contentBase: "./src",
    port: 3000,
    historyApiFallback: true
 },
...

The next one configures whether or not source maps should be used in the application. In our case, we will be using it only in development (you can change that, check out the docs to see how), copy the following code:

...
  devtool: debug ? "cheap-module-eval-source-map" : false,
...

Now we are going to tell webpack how to resolve our js and jsx imports in the application, copy the code below and add it to our config object:

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

Our next configuration is our modules which tells webpack how to load various types of files. Our module option has a rules option which accepts an array and we that array, we have our first rule that handles the loading of js/jsx files:

...
module: {
  rules: [      
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules)/,
        loader: "babel-loader",
        query: {
          presets: ["@babel/env", "@babel/preset-react"],
          plugins: [
            "@babel/plugin-proposal-class-properties",
            "@babel/plugin-syntax-dynamic-import",
            "@babel/plugin-proposal-object-rest-spread",
            ["@babel/plugin-proposal-decorators", { legacy: true }]
          ]
        }
      },
  ]
}
...

Our next rule is for loading SASS/SCSS/CSS files:

...
      {
        test: /\.(sa|sc|c)ss$/,
        use: debug
          ? [
              {
                loader: "style-loader"
              },
              {
                loader: "css-loader"
              },
              {
                loader: "sass-loader"
              }
            ]
          : [
              MiniCssExtractPlugin.loader,
              "css-loader",
              "sass-loader"
            ]
      },
...

Here we are telling webpack to use to different loaders based on the environment (if we are in development or production).

The next rule is for loading fonts and SVGs:

...
      {
        test: /\.(eot|ttf|woff|woff2|otf|svg)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 100000,
              name: "./assets/fonts/[name].[ext]"
              // publicPath: '../'
            }
          }
        ]
      },
...

And the last rule is for loading image assets (PNG,JPG, JPEG, GIF):

...
      {
        test: /\.(gif|png|jpe?g)$/i,
        use: [
          {
            loader: "file-loader",
            options: {
              outputPath: "assets/images/"
            }
          }
        ]
      }
...

Our next configuration is our plugins , for this option, we will be passing a different array based on the environment (development or production):

...
  plugins: debug
    ? [
        new CircularDependencyPlugin({
          // exclude detection of files based on a RegExp
          exclude: /a\.js|node_modules/,
          // add errors to webpack instead of warnings
          failOnError: true,
          // set the current working directory for displaying module paths
          cwd: process.cwd()
        }),
        new HtmlWebpackPlugin({
          template: "./src/index.html"
        })
      ]
    : [
        // define NODE_ENV to remove unnecessary code
        new webpack.DefinePlugin({
          "process.env.NODE_ENV": JSON.stringify("production")
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.AggressiveMergingPlugin(), // Merge chunks
        // extract imported css into own file
        new MiniCssExtractPlugin({
          // Options similar to the same options in webpackOptions.output
          // both options are optional
          filename: "[name].css",
          chunkFilename: "[id].css"
        }),
        new webpack.LoaderOptionsPlugin({
          minimize: true
        }),
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
        new HtmlWebpackPlugin({
          template: "./src/index.html"
          // minify: {
          //   collapseWhitespace: true,
          //   removeAttributeQuotes: false
          // }
        }),
        new CompressionPlugin({
          test: /\.(html|css|js|gif|svg|ico|woff|ttf|eot)$/,
          exclude: /(node_modules)/
        }),
        new BundleAnalyzerPlugin()
      ],
...

And for our final configuration which is optimization we are passing a new plugin that handles JS minimizations. So pass in the following code:

...
    minimizer: [
      new TerserPlugin({
        cache: true,
        parallel: true,
        sourceMap: true, // Must be set to true if using source-maps in production
        terserOptions: {
          ie8: true,
          safari10: true,
          sourceMap: true
        }
      })
    ]
...

If you were able to follow along, your configuration file should look like the one below:

const debug = process.env.NODE_ENV !== "production";
const webpack = require("webpack");
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CircularDependencyPlugin = require("circular-dependency-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {
    publicPath: "/",
    path: path.join(__dirname, "build"),
    filename: "js/[name].bundle.min.js",
    chunkFilename: "js/[name].bundle.js"
  },
  devServer: {
    inline: true,
    contentBase: "./src",
    port: 3000,
    historyApiFallback: true
  },
  devtool: debug ? "cheap-module-eval-source-map" : false,
  resolve: {
    extensions: [".js", ".jsx"]
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules)/,
        loader: "babel-loader",
        query: {
          presets: ["@babel/env", "@babel/preset-react"],
          plugins: [
            "@babel/plugin-proposal-class-properties",
            "@babel/plugin-syntax-dynamic-import",
            "@babel/plugin-proposal-object-rest-spread",
            ["@babel/plugin-proposal-decorators", { legacy: true }]
          ]
        }
      },
      {
        test: /\.(sa|sc|c)ss$/,
        use: debug
          ? [
              {
                loader: "style-loader"
              },
              {
                loader: "css-loader"
              },
              {
                loader: "sass-loader"
              }
            ]
          : [
              MiniCssExtractPlugin.loader,
              "css-loader",
              "sass-loader"
            ]
      },
      {
        test: /\.(eot|ttf|woff|woff2|otf|svg)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 100000,
              name: "./assets/fonts/[name].[ext]"
              // publicPath: '../'
            }
          }
        ]
      },
      {
        test: /\.(gif|png|jpe?g)$/i,
        use: [
          {
            loader: "file-loader",
            options: {
              outputPath: "assets/images/"
            }
          }
        ]
      }
    ]
  },
  plugins: debug
    ? [
        new CircularDependencyPlugin({
          // exclude detection of files based on a RegExp
          exclude: /a\.js|node_modules/,
          // add errors to webpack instead of warnings
          failOnError: true,
          // set the current working directory for displaying module paths
          cwd: process.cwd()
        }),
        new HtmlWebpackPlugin({
          template: "./src/index.html"
        })
      ]
    : [
        // define NODE_ENV to remove unnecessary code
        new webpack.DefinePlugin({
          "process.env.NODE_ENV": JSON.stringify("production")
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.AggressiveMergingPlugin(), // Merge chunks
        // extract imported css into own file
        new MiniCssExtractPlugin({
          // Options similar to the same options in webpackOptions.output
          // both options are optional
          filename: "[name].css",
          chunkFilename: "[id].css"
        }),
        new webpack.LoaderOptionsPlugin({
          minimize: true
        }),
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
        new HtmlWebpackPlugin({
          template: "./src/index.html"
          // minify: {
          //   collapseWhitespace: true,
          //   removeAttributeQuotes: false
          // }
        }),
        new CompressionPlugin({
          test: /\.(html|css|js|gif|svg|ico|woff|ttf|eot)$/,
          exclude: /(node_modules)/
        }),
        new BundleAnalyzerPlugin()
      ],
  optimization: {
    minimizer: [
      new TerserPlugin({
        cache: true,
        parallel: true,
        sourceMap: true, // Must be set to true if using source-maps in production
        terserOptions: {
          ie8: true,
          safari10: true,
          sourceMap: true
        }
      })
    ]
  }
};

Now try running the command below again:

npm run dev

Now when you navigate to http://localhost:3000 you should be able to see your application.

Wow! That was quite long, take some time to appreciate what you have just accomplished.

If you get lost anywhere during the reading or you need some clarification, feel free to reach out in the comments or create an issue on the repo (I might actually respond faster 😌).


Published by HackerNoon on 2019/05/30