Learn Webpack for React

You know React, you know create-react-app, but now you want to learn Webpack to create your own configurations and run React. In this tutorial, we will see the basics of Webpack for React to get you started, including React Router, React Fast Refresh, Code Splitting by Route and Vendor, production configuration, and more.

[topads][/topads]

Before we start, here’s the full list of features we are going to set up together in this tutorial:

  • React 17
  • React Fast Refresh
  • React Router 5
  • Webpack 5
  • Semantic UI as the CSS Framework
  • CSS Autoprefixer
  • CSS Modules
  • @babel/plugin-proposal-class-properties
  • @babel/plugin-syntax-dynamic-import
  • Code Splitting by Route and Vendor
  • Webpack Bundle Analyzer

Pre-requisites

Have the following pre-installed:

And you should have at least some basic knowledge of React and React Router.

Note: You can use npm if you wish, although the commands will vary slightly.

Initial Dependencies

Let us start by creating our directory and package.json.

In your terminal type the following:

mkdir webpack-for-react && cd $_
yarn init -y

This first command will create our directory and move into it, then we initialize a package.json accepting defaults.
If you inspect it you will see the bare bones configuration:

{
  "name": "webpack-for-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

Now we install our initial dependencies.

In your terminal type the following:

yarn add react react-dom prop-types react-router-dom semantic-ui-react semantic-ui-css

yarn add @babel/core babel-loader @babel/preset-env @babel/preset-react @babel/plugin-proposal-class-properties @babel/plugin-syntax-dynamic-import css-loader style-loader html-webpack-plugin webpack webpack-nano webpack-plugin-serve  -D

The development dependencies will only be used, as implied, during the development phase, and the production dependencies are what our application needs in production.

{
  "name": "webpack-for-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "react": "^17.0.1",,
    "react-dom": "^17.0.1",
    "prop-types": "^15.7.2",
    "react-router-dom": "^5.2.0",
    "semantic-ui-css": "^2.4.1",
    "semantic-ui-react": "^2.0.3"
  },
  "devDependencies": {
    "@babel/core": "^7.12.13",
    "@babel/plugin-proposal-class-properties": "^7.12.13",
    "@babel/plugin-syntax-dynamic-import": "^7.8.3",
    "@babel/preset-env": "^7.12.13",
    "@babel/preset-react": "^7.12.13",
    "css-loader": "^5.0.1",
    "html-webpack-plugin": "^5.0.0",
    "style-loader": "^2.0.0",
    "webpack": "^5.21.0",
    "webpack-nano": "^1.1.1",
    "webpack-plugin-serve": "^1.2.1"
  }
}

Note: Changes to previously created files will be highlighted.
Note: Dependencies versions might be different than yours from the time of this writing.

Setting up Babel

In the root directory (webpack-for-react) we create the Babel configuration file.

touch .babelrc

At this point you can open your favorite editor (mine is VS Code by the way), then point the editor to the root of this project and open .babelrc file and copy the following:

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-syntax-dynamic-import",
    "@babel/plugin-proposal-class-properties"
  ]
}

This tells Babel to use the presets (plugins) we previously installed. Later when we call babel-loader from Webpack, this is where it will look to know what to do.

Setting up Webpack

Now the fun begins! Let’s create the Webpack configuration file.

In your terminal type the following:

touch webpack.config.js

Open webpack.config.js and copy the following:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { WebpackPluginServe } = require('webpack-plugin-serve');

const port = process.env.PORT || 3000;

module.exports = {
  // Webpack configuration goes here
};

This is the basic shell for Webpack. We require webpackhtml-webpack-plugin and webpack-plugin-serve. Provide a default port if the environment variable PORT does not exist and export the module.

The following will be additions for webpack.config.js (one after another).

...
module.exports = {
  mode: 'development',
};

mode tells Webpack this configuration will be for either development or production. “Development Mode [is] optimized for speed and developer experience. production will give you a set of defaults useful for deploying your application (webpack 4: mode and optimization)”.

...
module.exports = {
  ...
  entry: ['./src/index.js', 'webpack-plugin-serve/client'],
  output: {
    filename: 'bundle.[fullhash].js',
  },
};

To get a running instance of Webpack we need:

  • entry — Specifies the entry point of your application; this is where your React app lives and where the bundling process will begin (Docs)

Webpack 4 introduced some defaults, so if you don’t include entry in your configuration, then Webpack will assume your entry point is located under the ./src directory, making entry optional as opposed to Webpack 3. For this tutorial, I have decided to leave entry as it makes it obvious where our entry point will be, but you are more than welcome to remove it if you so decide.

  • output — Tells Webpack how to write the compiled files to disk (Docs)
  • filename — This will be the filename of the bundled application. The [fullhash] portion of the filename will be replaced by a hash generated by Webpack every time your application changes and is recompiled (helps with caching).
...
module.exports = {
  ...
  devtool: 'inline-source-map',
};

devtool will create source maps to help you with debugging of your application. There are several types of source maps and this particular map (inline-source-map) is to be used only in development. (Refer to the docs for more options).

...
module.exports = {
  ...
  module: {
    rules: [

      // First Rule
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },

      // Second Rule
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader',
            options: {
              esModule: true,
            },
          },
          {
            loader: 'css-loader',
            options: {
              esModule: true,
              modules: {
                mode: 'local',
                exportLocalsConvention: 'camelCaseOnly',
                namedExport: true,
              },
            },
          },
        ],
      },
    ],
  },
  watch: true,
};
  • module — What types of modules your application will include, in our case we will support ESNext (Babel) and CSS Modules
  • rules — How we handle each different type of module

First Rule

We test for files with a .js extension excluding the node_modules directory and use Babel, via babel-loader, to transpile down to vanilla JavaScript (basically, looking for our React files).

Remember our configuration in .babelrc? This is where Babel looks at that file.

Second Rule

We test for CSS files with a .css extension. Here we use two loaders, style-loader and css-loader, to handle our CSS files. Then we configure the loaders to use CSS Modules (esModule), camel case (exportLocalsConvention) and create source maps.

This gives us the ability to use import Styles from ‘./styles.css’ syntax (or destructuring like this import { style1, style2 } from ‘./styles.css’).

In the React app we would write something like this:

...
import Styles from ‘./styles.css’
<div className={Styles.style1}>Hello World</div>

// or with the destructuring syntax

import { style1, style2 } from ‘./styles.css’
<div className={style1}>Hello World</div>
...

Camel case gives us the ability to write our CSS rules like this:

.home-button {...}

And use it in our React app like this:

...
import { homeButton } from './styles.css'
...
...
module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico'
    }),
    new WebpackPluginServe({
      host: 'localhost',
      port: port,
      historyFallback: true,
      open: true,
      liveReload: false,
      hmr: true,
      static: './dist',
    }),
  ],
};

This section is where we configure (as the name implies) plugins.

HtmlWebpackPlugin (html-webpack-plugin) accepts an object with different options. In our case, we specify the HTML template we will be using and the favicon. (Refer to the docs for more options).

WebpackPluginServe (webpack-plugin-serve) as mentioned above, is a Webpack development server.

We specify localhost as the host and assign the variable port as the port (if you remember, we assigned port 3000 to this variable). We set historyApiFallbac to true and open to true. This will open the browser automatically and launch your application in http://localhost:3000. liveReload and hmr to enable Fast Refresh (we will configure later). Finally static sets the directory from which static files will be served from the root of the application.

Later we will be adding other plugins for Bundle Analyzer and Fast Refresh.

Now, below is the complete Webpack configuration. (webpack.config.js):

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { WebpackPluginServe } = require('webpack-plugin-serve');

const port = process.env.PORT || 3000;

module.exports = {
  mode: 'development',
  entry: ['./src/index.js', 'webpack-plugin-serve/client'],
  output: {
    filename: 'bundle.[fullhash].js',
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader',
            options: {
              esModule: true,
            },
          },
          {
            loader: 'css-loader',
            options: {
              esModule: true,
              modules: {
                mode: 'local',
                exportLocalsConvention: 'camelCaseOnly',
                namedExport: true,
              },
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico',
    }),
    new WebpackPluginServe({
      host: 'localhost',
      port: port,
      historyFallback: true,
      open: true,
      liveReload: false,
      hmr: true,
      static: './dist',
    }),
  ],
  watch: true,
};

Creating the React App

We will be creating a simple Hello World app with three routes: a home, a page not found and a dynamic page that we will be loading asynchronously when we implement code splitting later.

Note: Assuming you have a basic understanding of React and React Router, I will not go into many details and only highlight what’s relevant to this tutorial.

We currently have the following project structure:

|-- node_modules
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock

In your terminal type the following:

mkdir public && cd $_
touch index.html

We create a public directory, move into it and also create an index.html file. Here is where we also have the favicon. You can grab it from here and copy it into public directory.

Open the index.html file and copy the following:

<!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">
  <link async rel="stylesheet" href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" />
  <title>webpack-for-react</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

Nothing much here (just a standard HTML template) only, we are adding the Semantic UI stylesheet and also creating a div with an ID of root. This is where our React app will render.

Back to your terminal type the following:

cd ..
mkdir src && cd $_
touch index.js

Open index.js and copy the following:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';

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

In your terminal type the following:

mkdir components && cd $_
touch App.js Layout.js Layout.css Home.js DynamicPage.js NoMatch.js

After creating the React component files, we have the following project structure:

|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- webpack.config.js
|-- yarn.lock

Open App.js and copy the following:

import React from 'react';
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom';

import Home from './Home';
import DynamicPage from './DynamicPage';
import NoMatch from './NoMatch';

const App = () => {
  return (
    <Router>
      <div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/dynamic" component={DynamicPage} />
          <Route component={NoMatch} />
        </Switch>
      </div>
    </Router>
  );
};

export default App;

We create our basic “shell” with React Router and have a homedynamic page and page not found route.

Open Layout.css and copy the following:

.pull-right {
  display: flex;
  justify-content: flex-end;
}
.h1 {
  margin-top: 10px !important;
  margin-bottom: 20px !important;
}

Open Layout.js and copy the following:

import React from 'react';
import { Link } from 'react-router-dom';
import { Header, Container, Divider, Icon } from 'semantic-ui-react';

import { pullRight, h1 } from './layout.css';

const Layout = ({ children }) => {
  return (
    <Container>
      <Link to="/">
        <Header as="h1" className={h1}>
          webpack-for-react
        </Header>
      </Link>
      {children}
      <Divider />
      <p className={pullRight}>
        Made with <Icon name="heart" color="red" /> by Esau Silva
      </p>
    </Container>
  );
};

export default Layout;

This is our container component where we define the layout of the site. Making use of CSS Modules, we are importing two CSS rules from layout.css. Also notice how we are using camel case for pullRight.

Open Home.js and copy the following:

import React from 'react';
import { Link } from 'react-router-dom';

import Layout from './Layout';

const Home = () => {
  return (
    <Layout>
      <p>Hello World of React and Webpack!</p>
      <p>
        <Link to="/dynamic">Navigate to Dynamic Page</Link>
      </p>
    </Layout>
  );
};

export default Home;

Open DynamicPage.js and copy the following:

import React from 'react';
import { Header } from 'semantic-ui-react';

import Layout from './Layout';

const DynamicPage = () => {
  return (
    <Layout>
      <Header as="h2">Dynamic Page</Header>
      <p>This page was loaded asynchronously!!!</p>
    </Layout>
  );
};

export default DynamicPage;

Open NoMatch.js and copy the following:

import React from 'react';
import { Icon, Header } from 'semantic-ui-react';

import Layout from './Layout';

const NoMatch = () => {
  return (
    <Layout>
      <Icon name="minus circle" size="big" />
      <strong>Page not found!</strong>
    </Layout>
  );
};

export default NoMatch;

We are done creating the React components. For a final step before running our application, open package.json:

{
  "name": "webpack-for-react",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "wp"
  },
  "dependencies": {
    "react": "^17.0.1",,
    "react-dom": "^17.0.1",
    "prop-types": "^15.7.2",
    "react-router-dom": "^5.2.0",
    "semantic-ui-css": "^2.4.1",
    "semantic-ui-react": "^2.0.3"
  },
  "devDependencies": {
    "@babel/core": "^7.12.13",
    "@babel/plugin-proposal-class-properties": "^7.12.13",
    "@babel/plugin-syntax-dynamic-import": "^7.8.3",
    "@babel/preset-env": "^7.12.13",
    "@babel/preset-react": "^7.12.13",
    "css-loader": "^5.0.1",
    "html-webpack-plugin": "^5.0.0",
    "style-loader": "^2.0.0",
    "webpack": "^5.21.0",
    "webpack-nano": "^1.1.1",
    "webpack-plugin-serve": "^1.2.1"
  }
}

We add the scripts key and also the start key. This will allow us to run React with the Webpack Development Server. If you don’t specify a configuration file, webpack-plugin-serve will look for webpack.config.js file as the default configuration entry within the root directory.

Before we run the app, copy the favicon.ico and place it under the public directory.

Now the moment of truth! Type the following in your terminal (remember to be in the root directory) and Yarn will call our start script.

yarn start
Running React
Running React

Now we have a working React app powered by our own Webpack configuration. Notice at the end of the GIF I am highlighting the bundled JavaScript file Webpack generated for us, and as we indicated in the configuration, the filename has a unique hash, main.d505bbab002262a9bc07.js.

Setting up Fast Refresh

From the root directory, add the following two packages:

yarn add @pmmmwh/react-refresh-webpack-plugin react-refresh -D

Open webpack.config.jsand add the highlighted lines (some code omitted for brevity)

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              plugins: [require('react-refresh/babel')].filter(Boolean),
            },
          },
        ],
      },
    ]
  }
  ...
  plugins: [
    new ReactRefreshWebpackPlugin({
      overlay: { sockIntegration: 'wps' },
    }),
  ]
}

Now we are ready to test Fast Refresh! Back in the terminal run your app, make a change and watch as the app updates without a full-page refresh.

yarn start
HMR in action
HMR in action

After updating the file, the page changes without a full refresh. To show this change in the browser I select Rendering -> Paint flashing in Chrome DevTools, which highlights the areas of the page, in green, that changed. I also highlight in Terminal the change Webpack sent to the browser to make this happen.

[signupform][/signupform]

Code Splitting

With code splitting, instead of having your application in one big bundle, you can have multiple bundles each loading asynchronously or in parallel. Also you can separate vendor code from you app code which can potentially decrease loading time.

By Route

There are several ways we can achieve code splitting by route, however in our case we will be using react-imported-component.

We would also like to show a loading spinner when the user navigates to a different route. This is a good practice as we don’t want the user to just stare at a blank screen while he/she waits for the new page to load. So, we will be creating a Loading component.

However, if the new page loads really fast, we don’t want the user to see a flashing loading spinner for a couple of milliseconds, so we will delay the Loading component by 300 milliseconds. To achieve this, we will be using React-Delay-Render.

Start by installing the two additional dependencies.

In your terminal type the following:

yarn add react-imported-component react-delay-render

Now we are going to create the Loading components.

In your terminal type the following:

touch ./src/components/Loading.js

Open Loading.js and copy the following:

import React from 'react';
import { Loader } from 'semantic-ui-react';
import ReactDelayRender from 'react-delay-render';

const Loading = () => <Loader active size="massive" />;

export default ReactDelayRender({ delay: 300 })(Loading);

Now that we have the Loading component, open App.js and modify it as follows:

import React from 'react';
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom';
import importedComponent from 'react-imported-component';

import Home from './Home';
import Loading from './Loading';

const AsyncDynamicPAge = importedComponent(
  () => import(/* webpackChunkName:'DynamicPage' */ './DynamicPage'),
  {
    LoadingComponent: Loading
  }
);
const AsyncNoMatch = importedComponent(
  () => import(/* webpackChunkName:'NoMatch' */ './NoMatch'),
  {
    LoadingComponent: Loading
  }
);

const App = () => {
  return (
    <Router>
      <div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/dynamic" component={AsyncDynamicPAge} />
          <Route component={AsyncNoMatch} />
        </Switch>
      </div>
    </Router>
  );
};

export default App;

This will create three bundles, or chunks, one for the DynamicPage component, one for the NoMatch component, and one for the main app.

Let’s also change the bundle filename. Open webpack.config.js and change it as follows:

...
module.exports = {
  ...
  output: {
    filename: '[name].[fullhash].js',
    ...
  },
}

It is time to run the app and take a look at code splitting by route in action.

yarn start
Code Splitting by Route
Code Splitting by Route

In the GIF, I first highlight the three different chunks created by Webpack in terminal. Then I highlight that upon the app launching, only the main chuck was loaded. Finally, we see that upon clicking Navigate to Dynamic Page the chunk corresponding to this page loaded asynchronously.

We also see that the chunk corresponding to the page not found was never loaded, saving the user bandwidth.

By Vendor

Now let’s split the application by vendor. We will be looking at splitting Semantic UI from the main app and into its own chunk. We will be using the SplitChunksPlugin.

Open webpack.config.js and make the following changes:

...
module.exports = {
  ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: /[\\/]node_modules[\\/]semantic-ui-([\S]+)[\\/]/,
          name: 'vendor',
          enforce: true,
        },
      },
    },
  },
  ...
};
  • optimization.splitChunks — Here we separate Semantic UI (/[\/]node_modules[\/]semantic-ui-([\S]+)[\/]/), name the chunk and name the chunk vendor
    • Note: If you wanted to extract all third party packages into a separate chunk, use /[\/]node_modules[\/]/

In the terminal, launch the app:

yarn start
Code Splitting by Vendor
Code Splitting by Vendor

In terminal, I highlight the three previous chunks plus the new vendor chunk. Then when we inspect the HTML we see that both vendor and app chunks were loaded.

Since we have made several updates to our Webpack configuration, below you will find the complete webpack.config.js file.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { WebpackPluginServe } = require('webpack-plugin-serve');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const port = process.env.PORT || 3000;

module.exports = {
  mode: 'development',
  entry: ['./src/index.js', 'webpack-plugin-serve/client'],
  output: {
    filename: '[name].[fullhash].js',
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              plugins: [require('react-refresh/babel')].filter(Boolean),
            },
          },
        ],
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader',
            options: {
              esModule: true,
            },
          },
          {
            loader: 'css-loader',
            options: {
              esModule: true,
              modules: {
                mode: 'local',
                exportLocalsConvention: 'camelCaseOnly',
                namedExport: true,
              },
            },
          },
        ],
      },
    ],
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: /[\\/]node_modules[\\/]semantic-ui-([\S]+)[\\/]/,
          name: 'vendor',
          enforce: true,
        },
      },
    },
  },
  plugins: [
    new ReactRefreshWebpackPlugin({
      overlay: { sockIntegration: 'wps' },
    }),
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico',
    }),
    new WebpackPluginServe({
      host: 'localhost',
      port: port,
      historyFallback: true,
      open: true,
      liveReload: false,
      hmr: true,
      static: './dist',
    }),
  ],
  watch: true,
};

Production Configuration

Rename the Webpack configuration from webpack.config.js to webpack.config.development.js. Then make a copy and name it webpack.config.production.js.

In your terminal type the following:

mv webpack.config.js webpack.config.development.js
cp webpack.config.development.js webpack.config.production.js

We will need a development dependency, mini-css-extract-plugin. From their docs: “It moves all the required *.css modules in entry chunks into a separate CSS file. So, your styles are no longer inlined into the JS bundle, but in a separate CSS file (styles.css). If your total stylesheet volume is big, it will be faster because the CSS bundle is loaded in parallel to the JS bundle.”

In your terminal type the following:

yarn add mini-css-extract-plugin -D

Open webpack.config.production.js and make the following highlighted changes:

Doing something different here…I will add explanations with inline comments.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  mode: 'production',
  entry: {
    app: './src/index.js',
  },
  output: {
    // We want to create the JavaScript bundles under a
    // 'static' directory
    filename: 'static/[name].[fullhash].js',

    // Absolute path to the desired output directory. In our
    //case a directory named 'dist'
    // '__dirname' is a Node variable that gives us the absolute
    // path to our current directory. Then with 'path.resolve' we
    // join directories
    // Webpack 4 assumes your output path will be './dist' so you
    // can just leave this
    // entry out.
    path: path.resolve(__dirname, 'dist'),

    publicPath: '/',
  },

  // Change to production source maps
  devtool: 'source-map',

  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
      {
        test: /\.css$/,

        use: [
          {
            // We configure 'MiniCssExtractPlugin'
            loader: MiniCssExtractPlugin.loader,
            options: {
              esModule: true,
            },
          },
          {
            loader: 'css-loader',
            options: {
              // Allows to configure how many loaders
              // before css-loader should be applied
              // to @import(ed) resources
              importLoaders: 1,

              // Create source maps for CSS files
              sourceMap: true,

              esModule: true,
              modules: {
                mode: 'local',
                exportLocalsConvention: 'camelCaseOnly',
                namedExport: true,
              },
            },
          },
          {
            // PostCSS will run before css-loader and will
            // minify and autoprefix our CSS rules.
            loader: 'postcss-loader',
          },
        ],
      },
    ],
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: /[\\/]node_modules[\\/]semantic-ui-([\S]+)[\\/]/,
          name: 'vendor',
          enforce: true,
        },
      },
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      favicon: 'public/favicon.ico',
    }),

    // Create the stylesheet under 'styles' directory
    new MiniCssExtractPlugin({
      filename: 'styles/[name].[fullhash].css',
    }),
  ],
};

Notice we removed the port variable, the plugins related to Fast Refresh and the Development Server.

Also since we added PostCSS to the production configuration, we need to install it and create a configuration file for it.

In your terminal type the following:

yarn add postcss postcss-loader autoprefixer cssnano postcss-preset-env -D
touch postcss.config.js

Open postcss.config.js and copy the following:

const postcssPresetEnv = require('postcss-preset-env');
module.exports = {
  plugins: [
    postcssPresetEnv({
      browsers: ['>0.25%', 'not ie 11', 'not op_mini all']
    }),
    require('cssnano')
  ]
};

Here we are specifying what browsers we want autoprefixer (Refer to the Docs for more options) to support and minifying the CSS output.

Now for the last step before we create our production build, we need to create a build script in package.json.

Open the file and make the following changes to the scripts section:

...
"scripts": {
  "dev": "wp --config webpack.config.development.js",
  "prebuild": "rimraf dist",
  "build": "cross-env NODE_ENV=production wp --config webpack.config.production.js"
},
...

First thing to notice here is that we changed the start script from start to dev, then we added two additional scripts, prebuild and build.

Finally, we are indicating which configuration to use when in development or production.

  • prebuild — Will run before the build script and delete the dist directory created by our last production build. We use the library rimraf for this
  • build — First we use cross-env library just in case somebody is using Windows. This way setting up environment variables with NODE_ENV will work and finally we specify the production configuration.

In your terminal install the two new dependencies we included in package.json:

yarn add rimraf cross-env -D

Before creating the production build, let us look at our new project structure:

|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- Loading.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- postcss.config.js
|-- webpack.config.development.js
|-- webpack.config.production.js
|-- yarn.lock

At last we can create our production bundle.

In your terminal type the following:

yarn build
Build production bundle
Build production bundle

As you noticed, after we ran the build script, Webpack created a dist directory containing our production ready app. Now inspect the files that were created and notice they are minified and each has a corresponding source map. You will also notice PostCSS has added autoprefixing to the CSS file.

Now we take our production files and fire up a Node server to serve our site, and this is the result:

Running the production build
Running the production build

Note: I am using this server in the GIF above to serve our production files.

At this point we have two working Webpack configurations, one for development and one for production. However, since both configurations are very similar, they share many of the same settings. If we wanted to add something else, we would have to add it to both configurations files. Let’s fix this inconvenience.

Webpack Composition

Let’s start by installing webpack-merge and Chalk as development dependencies.

In your terminal type the following:

yarn add webpack-merge chalk -D

We will also need a couple of new directories and a few new files.

In your terminal type the following:

mkdir -p build-utils/addons
cd build-utils
touch build-validations.js common-paths.js webpack.common.js webpack.dev.js webpack.prod.js

Now let’s look at our new project structure:

|-- build-utils
    |-- addons
    |-- build-validations.js
    |-- common-paths.js
    |-- webpack.common.js
    |-- webpack.dev.js
    |-- webpack.prod.js
|-- node_modules
|-- public
    |-- index.html
    |-- favicon.ico
|-- src
    |-- components
        |-- App.js
        |-- DynamicPage.js
        |-- Home.js
        |-- Layout.css
        |-- Layout.js
        |-- Loading.js
        |-- NoMatch.js
    |-- index.js
|-- .babelrc
|-- package.json
|-- postcss.config.js
|-- webpack.config.development.js
|-- webpack.config.production.js
|-- yarn.lock

Open common-paths.js and copy the following:

const path = require('path');
const PROJECT_ROOT = path.resolve(__dirname, '../');

module.exports = {
  projectRoot: PROJECT_ROOT,
  outputPath: path.join(PROJECT_ROOT, 'dist'),
  appEntry: path.join(PROJECT_ROOT, 'src')
};

Here we define, as the name implies, the common paths for our Webpack configurations. PROJECT_ROOT needs to look one directory up as we are working under build-utils directory (one level down from the actual root path in our project).

Open build-validations.js and copy the following:

const chalk = require('chalk');

const ERR_NO_ENV_FLAG = chalk.red(
  `You must pass an '--env [dev|prod]' flag into your build for webpack to work!`
);

module.exports = { ERR_NO_ENV_FLAG };

Later when we modify our package.json we will be requiring --env flag in the scripts. These validations are to verify that the flag is present; if not, it will throw an error.

In the next three files, we will be separating the Webpack configurations into configurations that are shared among development and production, configurations that are only for development and configurations only for production.

Open webpack.common.js and copy the following:

const commonPaths = require('./common-paths');

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

const config = {
  output: {
    path: commonPaths.outputPath,
    publicPath: '/',
  },
  target: 'web',
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: /[\\/]node_modules[\\/]semantic-ui-([\S]+)[\\/]/,
          name: 'vendor',
          enforce: true,
        },
      },
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: `public/index.html`,
      favicon: `public/favicon.ico`,
    }),
  ],
};

module.exports = config;

We basically extracted out what was shared among webpack.config.development.js and webpack.config.production.js and transferred it to this file. At the top we require common-paths.js to set the output.path.

Open webpack.dev.js and copy the following:

const commonPaths = require('./common-paths');

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const { WebpackPluginServe: Serve } = require('webpack-plugin-serve');

const port = process.env.PORT || 3000;

const config = {
  mode: 'development',
  entry: {
    app: [`${commonPaths.appEntry}/index.js`, 'webpack-plugin-serve/client'],
  },
  output: {
    filename: '[name].[fullhash].js',
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              plugins: [require('react-refresh/babel')].filter(Boolean),
            },
          },
        ],
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader',
            options: {
              esModule: true,
            },
          },
          {
            loader: 'css-loader',
            options: {
              esModule: true,
              modules: {
                mode: 'local',
                exportLocalsConvention: 'camelCaseOnly',
                namedExport: true,
              },
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new ReactRefreshWebpackPlugin({
      overlay: { sockIntegration: 'wps' },
    }),
    new Serve({
      historyFallback: true,
      liveReload: false,
      hmr: true,
      host: 'localhost',
      port: port,
      open: true,
      static: commonPaths.outputPath,
    }),
  ],
  watch: true,
};

module.exports = config;

This is the same concept as with the previous file. Here we extracted out development only configurations.

Open webpack.prod.js and copy the following:

const commonPaths = require('./common-paths');

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

const config = {
  mode: 'production',
  entry: {
    app: [`${commonPaths.appEntry}/index.js`],
  },
  output: {
    filename: 'static/[name].[fullhash].js',
  },
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              esModule: true,
            },
          },
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              sourceMap: true,
              esModule: true,
              modules: {
                mode: 'local',
                exportLocalsConvention: 'camelCaseOnly',
                namedExport: true,
              },
            },
          },
          {
            loader: 'postcss-loader',
          },
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'styles/[name].[fullhash].css',
    }),
  ],
};

module.exports = config;

We extracted out production only configurations.

Now that we have the shared configurations and the ones specific for development and production in separate files, it is time to put everything together.

In terminal, if you are still in build-utils directory, go up one level to the root of the project, then delete the previous Webpack configurations and create a new Webpack configuration. Name it webpack.config.js.

In your terminal type the following:

cd ..
rm webpack.config.development.js webpack.config.production.js
touch webpack.config.js

Before configuring webpack.config.js, let’s open package.json and update the scripts section.

Modify the section as follows:

...
"scripts": {
  "dev": "yarn prebuild && wp --env dev",
  "prebuild": "rimraf dist",
  "build": "cross-env NODE_ENV=production wp --env prod"
},
...

Since we removed the –config flag, Webpack will now be looking for the default configuration, which is webpack.config.js.

We also take advantage of webpack-nano‘s custom flags and create -env flag, which we will be reading in our Webpack configuration to distinguish between development or production configurations.

Open webpack.config.js and copy the following:

Explanations with inline comments.

const buildValidations = require('./build-utils/build-validations');
const commonConfig = require('./build-utils/webpack.common');
const argv = require('webpack-nano/argv');
const { merge } = require('webpack-merge');

// We can include Webpack plugins, through addons, that do
// not need to run every time we are developing.
// We will see an example when we set up 'Bundle Analyzer'
const addons = (/* string | string[] */ addonsArg) => {
  let addons = Array.isArray(addonsArg)
    ? addonsArg.filter((item) => item !== true)
    : [addonsArg].filter(Boolean);

  return addons.map((addonName) =>
    require(`./build-utils/addons/webpack.${addonName}.js`)
  );
};

module.exports = () => {
  // This is where we read the custom flag rom 'scripts'
  // section in 'package.json'.
  const { env, addons: addonsArg } = argv;

  if (!env) {
    throw new Error(buildValidations.ERR_NO_ENV_FLAG);
  }

  // Select which Webpack configuration to use; development
  // or production
  const envConfig = require(`./build-utils/webpack.${env}.js`);

  // 'webpack-merge' will combine our shared configurations, the
  // environment specific configurations and any addons we are
  // including
  const mergedConfig = merge(commonConfig, envConfig, ...addons(addonsArg));

  return mergedConfig;
};

Now, this might seem like a lot of setup, but in the long run, it will come in handy.

At this time, you can launch the application or build the production files, and everything will function as expected (sorry, no GIF this time).

yarn dev
yarn build

Note: This “Webpack Composition” technique was taken from Webpack Academy, a free course by Sean Larkin which I recommend taking to learn more about Webpack, not specific to React.

BONUS: Setting up Webpack Bundle Analyzer

You don’t necessarily need Webpack Bundle Analyzer, but it does comes in handy when trying to optimize your builds.

Start by installing the dependency and creating the configuration file.

In your terminal type the following:

yarn add webpack-bundle-analyzer -D
touch build-utils/addons/webpack.bundleanalyzer.js

Open webpack.bundleanalyzer.js and copy the following:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'server'
    })
  ]
};

We are just exporting the plugins section, which includes Bundle Analyzer, for Webpack. Then webpack-merge will combine it into the final Webpack configuration. Remember the addons in webpack.config.js? Well, this is where it comes into place.

For the final step, let’s open package.json and include the new scripts as follows:

"scripts": {
  "dev": "yarn prebuild && wp --env dev",
  "dev:bundleanalyzer": "yarn prebuild && yarn dev --addons bundleanalyzer",
  "prebuild": "rimraf dist",
  "build": "cross-env NODE_ENV=production wp --env prod",
  "build:bundleanalyzer": "yarn build --addons bundleanalyzer"
},
  • dev:bundleanalyzer — Calls the dev script and passes a new environment variable addons=bundleanalyzer
  • build:bundleanalyzer — Calls the build script and passes a new environment variable addons=bundleanalyzer

Time to run the app with the bundle analyzer addon.

In your terminal type the following:

yarn dev:bundleanalyzer
Webpack Bundle Abalyzer
Webpack Bundle Abalyzer

The application launches alongside Webpack Bundle Analyzer.

Including addons with Webpack Composition can be very useful, as there are many plugins that you would want to use only at certain times.

Conclusion

First of all, you can get the full code on the GitHub repository.

Well, you made it to the end. Congratulations!! ? Now that you know the basics (and a little more) of Webpack for React, you can go ahead and keep exploring and learning more advanced features and techniques.

Thank you for reading and I hope you enjoyed it. If you have any questions, suggestions or corrections let me know in the comments below. Don’t forget to give this article a Share and some Claps ??.

You can follow me on TwitterGitHubLinkedIn or all of them.


Consider giving back by getting me a coffee (or a couple) by clicking the following button:

[bottomads][/bottomads]

Spread the love

4 thoughts on “Learn Webpack for React

  1. Anil says:

    Really Good one.. !! Solve almost every issues.

    Just want to know is there any article same for Server side rendering with React.

    There are other but most of them are not compatible with Babel7 /webpack 4.

    It would be really help you write or share any code base for understanding SSR on react.

  2. Ava says:

    Thank you very much! You save me a lot of headache and time Work like a charm!
    Why can’t babel read the jsconfig file ?

    • jgezau says:

      Thank you! You can use .babelrc to configure Babel, alternatively, you can use your package.json file for the configurations. In Babel 7 they introduced a new Project-Wide configuration, you can read more about it here

  3. Anon says:

    Lovely, keep going.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.