Last year I had to upgrade Webpack to 4 and Babel to 7, which even though it sounds trivial the upgrade included a lot of breaking changes. The new mode option amalgamating your production and development setup into one, switching libraries as some did not make the transition to 4 and a general reworking of how I laid out the original into what is presented below.

After which I'll break down what each component does.

webpack.config.js
// webpack.config.js
const path = require('path');

const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const WorkboxPlugin = require('workbox-webpack-plugin');

const devMode = process.env.NODE_ENV !== 'production';

const BUILD_DIR = path.resolve(__dirname, 'build');
const SRC_DIR = path.resolve(__dirname, 'src');

const isHot = path.basename(require.main.filename) === 'webpack-dev-server.js';

console.log(isHot)

console.log('BUILD_DIR', BUILD_DIR);
console.log('SRC_DIR', SRC_DIR);

const cssPlugin = new MiniCssExtractPlugin({
  filename: "style.css"
});

const htmlPlugin = new HtmlWebpackPlugin({
  template: "./public/index.html",
  filename: "./index.html"
});

const copyWebpack = new CopyWebpackPlugin([
    {from: './public/images', to: 'images'},
    {from: './public/fonts', to: 'fonts'},
    {from: './node_modules/font-awesome/fonts', to: 'fonts'},
    {from: './public/images/favicon.ico' },
    {from: './public/images/apple-icon.png' },
    {from: './public/robots.txt' }
  ],
  {copyUnmodified: false}
);

const uglifyJs = new UglifyJsPlugin({
  parallel: 4
});

const workbox = new WorkboxPlugin.GenerateSW({
  // these options encourage the ServiceWorkers to get in there fast
  // and not allow any straggling "old" SWs to hang around
  clientsClaim: true,
  skipWaiting: true
})

module.exports = {
    target: 'web',
    entry: {
      index: [SRC_DIR + '/index.js']
    },
    output: {
      publicPath: '/',
      path: BUILD_DIR,
      filename: '[name].bundle.js'
    },
    devtool: 'source-map',
    devServer: {
      port: 3000,
      disableHostCheck: true,
      host: 'localhost',
      contentBase: BUILD_DIR,
      historyApiFallback: true,
      compress: true,
      hot: true,
      open: true,
      proxy: {
        '/api/*': 'http://localhost:5000',
        '/media/*': 'http://localhost:5000'
      }
    },
    module : {
        rules : [
            {
                test: /\.s?[ac]ss$/,
                use: [
                    isHot ? "style-loader" : MiniCssExtractPlugin.loader,
                    { loader: 'css-loader', options: { url: false, sourceMap: true } },
                    { loader: 'sass-loader', options: { sourceMap: true } }
                ],
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                  loader: "babel-loader"
                }
            }
        ]
    },
    plugins: this.mode === 'production'
     ? [cssPlugin, htmlPlugin, copyWebpack, workbox, uglifyJs]
     : [cssPlugin, htmlPlugin, copyWebpack, workbox]
    ,
    mode : devMode ? 'development' : 'production'
};
.babelrc
// .babelrc
{
  "presets": [
    "@babel/preset-react",
    [ "@babel/preset-env", {
      "targets": {
        "browsers": [
          ">0.25%",
          "not op_mini all"
        ]
      }
    }]
  ],
  "plugins": [
    "@babel/plugin-proposal-object-rest-spread",
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-runtime"
  ]
}

Breakdown

Build & Src Directories

First I define constants to store where the source is (/src) and compiled build (via webpack-dev-tools & webpack, in /build), we'll need those later.

const BUILD_DIR = path.resolve(__dirname, 'build');
const SRC_DIR = path.resolve(__dirname, 'src');

Hot Loading

For hot reloading, when the code changes refresh the client, I had a problem with MiniCssExtractPlugin not handling it. So I setup a constant that if dev-server is loaded it will run the appropriate styling libraries.

const isHot = path.basename(require.main.filename) === 'webpack-dev-server.js';

use: [
    isHot ? "style-loader" : MiniCssExtractPlugin.loader,
    { loader: 'css-loader', options: { url: false, sourceMap: true } },
    { loader: 'sass-loader', options: { sourceMap: true } }
],

CSS & HTML Plugins

Next defining the css extractor and html plugins, again defining the setup inside a constant. A really handy method that cleans up how they're implemented later on under 'plugins'.

const cssPlugin = new MiniCssExtractPlugin({
  filename: "style.css"
});

const htmlPlugin = new HtmlWebpackPlugin({
  template: "./public/index.html",
  filename: "./index.html"
});
plugins: this.mode === 'production'
 ? [cssPlugin, htmlPlugin, copyWebpack, workbox, uglifyJs]
 : [cssPlugin, htmlPlugin, copyWebpack, workbox]
,

I don't use uglifyjs in development as there's no need, and it'll massively slow down hot-loading.

CopyWebpackPlugin

For CopyWebpackPlugin I setup the usual copy definitions for images, fonts and font-awesome so everything's self hosted.

Then for favicons and the robots.txt file which usually reside in the root I set the from but not the to so it will default to copying to the root location of the website (correct me if i'm wrong).

const copyWebpack = new CopyWebpackPlugin([
    {from: './public/images', to: 'images'},
    {from: './public/fonts', to: 'fonts'},
    {from: './node_modules/font-awesome/fonts', to: 'fonts'},
    {from: './public/images/favicon.ico' },
    {from: './public/images/apple-icon.png' },
    {from: './public/robots.txt' }
  ],
  {copyUnmodified: false}
);

copyUnmodified's default value is false which: "Copies files, regardless of modification when using watch or webpack-dev-server. All files are copied on first build, regardless of this option".

uglifyJs

Next setup uglifyJs, this obfuscates your end result making it difficult for others to reverse engineer.

As I usually have more than one core on my computer I set it up to use parallel processing using 4 threads as this is a very cpu intensive task.

const uglifyJs = new UglifyJsPlugin({
  parallel: 4
});

WorkboxPlugin

WorkboxPlugin is a tool to implement service workers inside your web app and enable caching so your application has the chance to operate even when in offline mode and then gracefully connect when back online.

const workbox = new WorkboxPlugin.GenerateSW({
  // these options encourage the ServiceWorkers to get in there fast
  // and not allow any straggling "old" SWs to hang around
  clientsClaim: true,
  skipWaiting: true
})

You can read more here https://webpack.js.org/guides/progressive-web-application/#adding-workbox.

devServer

While in development I have the server running a separate process on port 5000 with the client at 3000. So I setup proxy's for the /api and /media uploads.

I enable compression, hot loading and set the hostname. With that as so as the client starts up an event will be fired to the browser to hot-load the site on http://localhost:3000

devServer: {
  port: 3000,
  disableHostCheck: true,
  host: 'localhost',
  contentBase: BUILD_DIR,
  historyApiFallback: true,
  compress: true,
  hot: true,
  open: true,
  proxy: {
    '/api/*': 'http://localhost:5000',
    '/media/*': 'http://localhost:5000'
  }
},

modes

With WebPack 4 it introduced the concept of build modes so we only had one webpack file to maintain.

I generate a constant that holds the current mode taken from the current process.env.NODE_ENV node environment.

const devMode = process.env.NODE_ENV !== 'production';

Which I can then pass over to mode, plugins and what else needs it.

plugins: this.mode === 'production'
 ? [cssPlugin, htmlPlugin, copyWebpack, workbox, uglifyJs]
 : [cssPlugin, htmlPlugin, copyWebpack, workbox]
,
mode : devMode ? 'development' : 'production'

Babel

For Babel I had to change things with version 7.

// .babelrc 6
{
  "presets": [
    "react",
    "env"
  ],
  "plugins": [
    "transform-object-rest-spread",
    "transform-class-properties",
    "transform-runtime"
  ]
}

Still loading support for spread operator (@babel/plugin-proposal-object-rest-spread), class properties (@babel/plugin-proposal-class-properties) and transform which adds common helpers across your compiled app that are deduplicated to help with code size (@babel/plugin-transform-runtime)

// .babelrc 7
{
  "presets": [
    "@babel/preset-react",
    [ "@babel/preset-env", {
      "targets": {
        "browsers": [
          ">0.25%",
          "not op_mini all"
        ]
      }
    }]
  ],
  "plugins": [
    "@babel/plugin-proposal-object-rest-spread",
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-transform-runtime"
  ]
}

The juicy part is:

"@babel/preset-react",
[ "@babel/preset-env", {
  "targets": {
    "browsers": [
      ">0.25%",
      "not op_mini all"
    ]
  }
}]

Which rather than compile to support all browsers which increases code size, it limits support to the top 25% of all current browsers.

Removing support for opera mini on all versions.

not op_mini all

This is a browserlist query, you can read more about it here https://github.com/browserslist/browserslist.