Scaffolding React web app with Webpack 4

The github repo, react-webpack, has the minimal scaffolding for a React web app with Webpack 4. The tutorial explains how the project was built.

New project

Create a new project

yarn init

Install Webpack as devDependency.

yarn add webpack webpack-cli webpack-dev-server --dev

Create Webpack configuration file:

const path = require('path'); 
module.exports = {     
  entry: './src/index.js',     
  output: {         
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: '/'  
  } 
}

Add two scripts to package.json

"scripts": {
  "start": "webpack-dev-server",
  "build": "webpack"
}

Place an index.js file in src folder which serves as input to the build process.

console.log('Hello world');

Run the build command. This creates a dist folder with bundle.js file.

Next, we will create React components and process it using babel.

Processing React components

Install React.

yarn add react react-dom

Create two folders, src and dist. The src folder contains our app. And dist folder contains the output of the build process. In src folder, we create the index.js file, the entry point to the build process.

import React from 'react'; 
import ReactDOM from 'react-dom'; 
import Home from './Home'; 

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

React DOM renders the Home component in the div element with id root. Create a new file, Home.js.

import React, { Component } from 'react'; 
export default class Home extends Component { 
  render() {
    return (
      <h1>Hello world</h1>
    );
  }
} 

Babel transpiles ES2015+ code to JavaScript 5. It also has a react preset to convert JSX code in React to plain JavaScript. Add babel packages as devDependency.

yarn add @babel/core babel-loader @babel/preset-env @babel/preset-react --dev

In the build configuration file, use the babel-loader to process JSX files. In webpack config, there are rules to specify how to process a module (file). Specify a rule to process JS files.

module: {
    rules: [
        {
            test: /.js$/,
            loader: 'babel-loader',
            include: path.resolve(__dirname, 'src'),
            exclude: /node_modules/,
            options: {
                presets: ['@babel/preset-env', '@babel/preset-react']
            }
        }
    ]
}

There are two babel presets. To convert new JS syntax to browser compatible JS syntax, we use env preset. To convert React components in JSX to plain JavaScript, we use react preset.

Add an index.html file to src folder.

<html>
    <head>
        <title>React with Webpack</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
        <div id="root"></div>
    </body>
</html>

There is a HTML webpack plugin. This plugin moves the HTML file to the output folder and inserts script tags for the output bundle. Install the plugin.

yarn add html-webpack-plugin --dev

In webpack.config, add a require statement at the top.

const HtmlWebpackPlugin = require('html-webpack-plugin');

Add a new plugins config section below the module config.

plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        })
    ]

Do a build. In the dist folder, there is a new index.html file. Run the start command. Webpack spins a new devServer. Browse to localhost:8080 and view the webapp. You should see the Hello world text prominently.

yarn build
yarn start

Process styles

We will continue our exploration of Webpack. In Home.js, add a className to the div tag.

import React, { Component } from 'react';

export default class Home extends Component {
    render() {
        return (
            <div className="container">
                <h1>Hello world</h1>
            </div>
        );
    }
}

Create a new file, home.scss.

.container {
  min-height: 100vh;
  background-color: #eee;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column
}

Add an import statement to the top of Home.js

import './home.scss';

To process scss file, we need more loaders.

The container has the background image. The background image is available in the images folder placed along the src folder.

{
    test: /\.scss$/,
    use: ['style-loader', 'css-loader', 'sass-loader']
}

These loaders process the module (scss file) from right to left. sass-loader takes the help of node-sass package to convert scss syntax to css syntax understood by browsers. css-loader processes the resultant css. style-loader drops all the styles in the browser in the header element.

yarn add node-sass sass-loader css-loader style-loader --dev

Process images

Download the following image to the project.

add this image

Rename the file as trek.jpeg in a new images folder (peer to src). Add the image to Home.js.

import Trek from '../images/trek.jpeg';

Insert the image tag below the h1 tag.

<img src={Trek} />

To process images in the build, we need another loader, file-loader.

yarn add file-loader --dev

Add a new rule to process images.

{
    test: /\.(jpg|jpeg|png|svg)$/,
    use: [
        {
            loader: 'file-loader',
            options: {
                name: '[path][name].[ext]'
            }
        }
    ]
}

The file-loader emits the image file in the output folder. By default, it will emit the file in the output folder as [hash].[ext] to maintain uniqueness. We can override the behaviour by providing a name option. With the option specified above, the file will be emitted in the output folder with its original name and path.

Start the webpack server and open localhost:8080.

Final Output

Next, we will explore how hot reloading works.

Hot reloading

By default, Webpack does live reloading. When the contents of the app changes, the page reloads. For a good development experience, the page should not reload. Rather, only the affected components should be updated. Hot reloading does that. There is a lot of theory behind how hot reloading works. In this tutorial, we will configure our web app so that both styles and javascript are “hot reloaded” without a browser refresh.

Import webpack at the top of webpack config.

const webpack = require('webpack');

Add the HotModuleReplacementPlugin to the plugins section.

new webpack.HotModuleReplacementPlugin()

Add a devServer config section.

devServer: {
  contentBase: path.join(__dirname, 'dist'),
  hot: true
}

Webpack is configured with hot reloading. By default, the style-loader has HMR (hot module replacement) enabled. So, all your style changes will be reflected in the page instantaneously. Next, we enable HMR for React components.

Install react-hot-loader package.

yarn add react-hot-loader

Add a babel plugin (react-hot-loader/babel) to process JavaScript files.

{
    test: /.jsx?$/,
    loader: 'babel-loader',
    include: path.resolve(__dirname, 'src'),
    exclude: /node_modules/,
    options: {
        presets: ['@babel/preset-env', '@babel/preset-react'],
        plugins: ['react-hot-loader/babel']
    }
},

Import hot from the root component (Home.js).

import { hot } from 'react-hot-loader/root'

Wrap the root component with hot.

export default hot(Home);

Additional steps

In addition to the above webpack configuration, I will recommend some more:

  1. Linting using eslint-loader.
  2. Add postcss-loader and autoprefixer to prefix CSS.
  3. Provide sourcemaps for debugging.

Production configuration

For generating the production build, create a new configuration file, webpack.production.config.js. We do not need the following from the development config.

  1. Hot reloading
  2. DevServer

Use the following config as starting point.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'production',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
        publicPath: '/'
    },
    module: {
        rules: [
            {
                test: /.jsx?$/,
                loader: 'babel-loader',
                include: path.resolve(__dirname, 'src'),
                exclude: /node_modules/,
                options: {
                    presets: ['@babel/preset-env', '@babel/preset-react']
                }
            },
            {
                test: /\.scss$/,
                use: ['style-loader', 'css-loader', 'sass-loader']
            },
            {
                test: /\.(jpg|jpeg|png|svg)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[path][name].[ext]'
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        })
    ],
};

For production, we won’t use webpack-dev-server. Instead, we will host the dist folder in any static website hosting provider like Firebase hosting or Netlify or Github pages.

Update the build script to use the production config.

webpack -p --config webpack.production.config.js

Extract CSS

With style-loader, CSS is injected into a style tag in the head section of the HTML document. For production, the CSS should be in a separate file.

Add mini-css-extract-plugin

yarn add mini-css-extract-plugin --dev

Import the plugin at the top of the config file.

const ExtractPlugin = require('mini-css-extract-plugin');

Add the plugin to the plugins section.

new ExtractPlugin(),

Use the loader for processing SCSS files.

{
    test: /\.scss$/,
    use: [ExtractPlugin.loader, 'css-loader', 'sass-loader']
},

Do a yarn build. And you should see the following dist folder.

dist folder

All the CSS is available in main.css. For following the tutorial, please use the Git repo.

Related Posts

Leave a Reply

Your email address will not be published.