Controlling Your Project: An In-Depth Look at Webpack and React, Part 2

This React-Webpack tutorial builds upon the basics of loaders and optimization covered previously, delving into more specialized React/Webpack configuration scenarios.

Integrating TypeScript with React Using Babel

While ts-loader offers a way to utilize TypeScript in React projects, this section focuses on transpiling TypeScript using @babel/preset-typescript. This approach is particularly advantageous as numerous libraries provide Babel plugins for compile-time optimization. Additionally, it allows leveraging Babel plugins from libraries like styled-components or react-intl, expanding the project’s capabilities.

The initial step involves installing the required TypeScript and Babel dependencies:

1
npm install -D typescript @babel/preset-typescript @types/react @types/react-dom

Next, a TypeScript configuration file is generated using the tsc command-line tool:

1
./node_modules/.bin/tsc -init --lib dom --jsx react --isolatedModules

This command produces a tsconfig.json file tailored for browser-based code. The --isolatedModules flag enforces compatibility with @babel/plugin-transform-typescript by imposing constraints on code structure. This proves beneficial by enabling IDE warnings for code that Babel might struggle to transform.

Subsequently, babel.config.js is modified to include a new preset:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@@ -6,7 +6,8 @@ module.exports = {
         modules: false
       }
     ],
-    "@babel/preset-react"
+    "@babel/preset-react",
+    "@babel/preset-typescript"
   ],
   plugins: [
     "@babel/plugin-transform-runtime",

Additionally, the .ts file extension is enabled within webpack.config.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@@ -11,7 +11,7 @@ module.exports = function(_env, argv) {

   return {
     devtool: isDevelopment && "cheap-module-source-map",
-    entry: "./src/index.js",
+    entry: "./src/index.tsx",
     output: {
       path: path.resolve(__dirname, "dist"),
       filename: "assets/js/[name].[contenthash:8].js",
@@ -20,7 +20,7 @@ module.exports = function(_env, argv) {
     module: {
       rules: [
         {
-          test: /\.jsx?$/,
+          test: /\.(js|jsx|ts|tsx)$/,
           exclude: /node_modules/,
           use: {
             loader: "babel-loader",
@@ -61,6 +61,9 @@ module.exports = function(_env, argv) {
         }
       ]
     },
+    resolve: {
+      extensions: [".js", ".jsx", ".ts", ".tsx"]
+    },
     plugins: [
       isProduction &&
         new MiniCssExtractPlugin({

While this configuration enables code transpilation, it lacks type validation. To address this, type checking is performed as a separate, concurrent process using fork-ts-checker-webpack-plugin.

Installation is the first step:

1
npm install -D fork-ts-checker-webpack-plugin

It’s then incorporated into the plugins section of webpack.config.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@@ -4,6 +4,7 @@ const HtmlWebpackPlugin = require("html-webpack-plugin");
 const webpack = require("webpack");
 const TerserWebpackPlugin = require("terser-webpack-plugin");
 const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin");
+const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");

 module.exports = function(_env, argv) {
   const isProduction = argv.mode === "production";
@@ -78,6 +79,9 @@ module.exports = function(_env, argv) {
         "process.env.NODE_ENV": JSON.stringify(
           isProduction ? "production" : "development"
         )
+      }),
+      new ForkTsCheckerWebpackPlugin({
+        async: false
       })
     ].filter(Boolean),
     optimization: {

The async: false setting prevents Webpack from generating erroneous code and displays compilation errors within an overlay during development server execution.

Note: Exploring Babel macros might also be worthwhile, considering their growing popularity.

Enhancing CSS Handling through Webpack

Building on the basic styling with css-loader discussed earlier, this section explores ways to enhance the configuration.

The proposed setup leverages the capabilities of CSS Modules, Sass, and PostCSS. While these technologies complement each other to some extent, they need not be used together. The final configuration presented here will include all three, granting you the flexibility to omit any unnecessary components.

CSS Modules

CSS Modules tackle the challenge of global scoping in CSS by assigning each class a unique, randomized name. For JavaScript files utilizing CSS Modules, an object exported by the loader maps original class names to their randomized counterparts. This mechanism enables the use of CSS classes while minimizing the risk of unintended collisions.

Support for CSS Modules is already present in css-loader. A new rule is added to explicitly activate CSS Modules when needed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@@ -33,11 +33,25 @@ module.exports = function(_env, argv) {
         },
         {
           test: /\.css$/,
           use: [
             isProduction ? MiniCssExtractPlugin.loader : "style-loader",
             "css-loader"
           ]
         },
+        {
+          test: /\.module.css$/,
+          use: [
+            isProduction ? MiniCssExtractPlugin.loader : "style-loader",
+            {
+              loader: "css-loader",
+              options: {
+                modules: true
+              }
+            }
+          ]
+        },
         {
           test: /\.(png|jpg|gif)$/i,
           use: {

This ensures that any file with a .module.css extension is processed with CSS Modules enabled.

PostCSS

PostCSS, an extensible CSS processing framework, boasts a vast plugin ecosystem for extending CSS syntax, performing optimization, and ensuring compatibility with older browsers.

The first step involves installing the required dependencies:

1
npm install -D postcss-loader postcss-import postcss-preset-env

Next, the CSS configuration is updated:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@@ -47,9 +47,11 @@ module.exports = function(_env, argv) {
             {
               loader: "css-loader",
               options: {
-                modules: true
+                modules: true,
+                importLoaders: 1
               }
-            }
+            },
+            "postcss-loader"
           ]
         },
         {

PostCSS will be configured with the following plugins:

  • postcss-import: Allows PostCSS to handle @import statements.
  • postcss-preset-env: Introduces polyfills to support modern CSS features across various browsers.

A new file named postcss.config.js is created and populated with the following content:

1
2
3
4
5
6
module.exports = {
  plugins: {
    "postcss-import": {},
    "postcss-preset-env": {}
  }
};

Feel free to explore the PostCSS plugin directory for additional extensions that might prove beneficial and incorporate them into your configuration.

Sass/SCSS

Sass stands as another widely-used CSS processing framework. Unlike the minimalist approach of PostCSS, Sass is “batteries included.” It natively provides support for nested rules, mixins, and rewriting rules for backward compatibility. While PostCSS focuses on preserving standard CSS syntax, Sass may deviate from the CSS specification. Nevertheless, Sass’s ubiquity makes it a convenient, albeit potentially limiting, option for writing CSS—a trade-off worth considering: depends on your requirements.

Begin by installing the necessary dependencies:

1
npm install -D sass-loader node-sass resolve-url-loader

Subsequently, integrate a new loader into the Webpack configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@@ -38,6 +38,25 @@ module.exports = function(_env, argv) {
             "css-loader"
           ]
         },
+        {
+          test: /\.s[ac]ss$/,
+          use: [
+            isProduction ? MiniCssExtractPlugin.loader : "style-loader",
+            {
+              loader: "css-loader",
+              options: {
+                importLoaders: 2
+              }
+            },
+            "resolve-url-loader",
+            {
+              loader: "sass-loader",
+              options: {
+                sourceMap: true
+              }
+            }
+          ]
+        },
         {
           test: /\.(png|jpg|gif)$/i,
           use: {

This code snippet proactively addresses a couple of potential issues:

  1. resolve-url-loader is placed after sass-loader to ensure that relative imports function correctly within @import-ed Sass files.

  2. The importLoaders option is set for css-loader to process @import-ed files using subsequent loaders in the chain.

With this configuration, you can author styles using Sass/SCSS, along with PostCSS and CSS Modules, as previously described. Although these options can coexist, it’s not mandatory to utilize them all within a single project. Select the tool that aligns best with your specific requirements.

Leveraging Web Workers

Web workers constitute a powerful concept in modern web development, enabling the offloading of computationally expensive tasks from the main thread. Employ web workers judiciously, reserving them for operations that cannot be optimized through intelligent scheduling within the event loop. They excel at optimizing lengthy, synchronous processes.

Webpack simplifies web worker utilization through worker-loader, which bundles worker files into the output directory and provides a worker class to the consuming file.

Installation of worker-loader is the first step:

1
npm install -D worker-loader

Next, add it to the configuration file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@@ -31,6 +31,10 @@ module.exports = function(_env, argv) {
             }
           }
         },
+        {
+          test: /\.worker\.js$/,
+          loader: "worker-loader"
+        },
         {
           test: /\.css$/,
           use: [

Now, to start using web workers, simply instantiate a class imported from a file ending in .worker.js that implements the standard Worker API interface.

Implementing Service Workers

Service workers unlock advanced optimization techniques and elevate the user experience. They allow your application to function offline when a user loses network connectivity and facilitate near-instantaneous loading even after updates.

Webpack, with the aid of the workbox-webpack-plugin module, simplifies the configuration of service workers. Install the module first:

1
npm install -D workbox-webpack-plugin

Then, incorporate the plugin into the plugins section of your Webpack configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@@ -4,6 +4,7 @@ const HtmlWebpackPlugin = require("html-webpack-plugin");
 const webpack = require("webpack");
 const TerserWebpackPlugin = require("terser-webpack-plugin");
 const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin");
+const WorkboxPlugin = require("workbox-webpack-plugin");

 module.exports = function(_env, argv) {
   const isProduction = argv.mode === "production";
@@ -75,6 +76,11 @@ module.exports = function(_env, argv) {
         "process.env.NODE_ENV": JSON.stringify(
           isProduction ? "production" : "development"
         )
+      }),
+      new WorkboxPlugin.GenerateSW({
+        swDest: "service-worker.js",
+        clientsClaim: true,
+        skipWaiting: true
       })
     ].filter(Boolean),
     optimization: {

This configuration employs the following options:

  • swDest: Dictates the output filename for the generated service worker file.
  • clientsClaim: Instructs the service worker to assume control of the page immediately upon registration, serving cached resources without waiting for the subsequent page reload.
  • skipWaiting: Enables immediate application of service worker updates, eliminating the need to wait for all active instances to terminate.

There’s a reason these latter options aren’t enabled by default. When used together, they introduce the potential for glitches to occur in time-sensitive situations. Therefore, it’s crucial to carefully consider whether to activate these options in your configuration.

Finally, register the service worker upon app initialization:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@@ -2,3 +2,9 @@ import React from "react";
 import ReactDOM from "react-dom";

 ReactDOM.render(<h3>React App</h3>, document.getElementById("root"));
+
+if ("serviceWorker" in navigator) {
+  window.addEventListener("load", () => {
+    navigator.serviceWorker.register("/service-worker.js");
+  });
+}

Service workers offer functionality beyond enabling offline capabilities. If finer control over service worker behavior is desired, consider employing the InjectManifest plugin. By crafting a custom service worker file, you can implement caching for API requests and leverage other features like push notifications. For a deeper understanding of Workbox’s capabilities, consult the Advanced Recipes section of its official documentation.

Advanced React Webpack Configuration: Empowering Your Project

This second installment in the Webpack tutorial series aimed to equip you with the knowledge to extend your Webpack configuration beyond common React use cases. It is my hope that this information proves valuable and empowers you to confidently tailor your configurations to meet project-specific goals.

As always, the complete configuration files on GitHub is readily available, alongside the comprehensive Webpack documentation and its plugins section, to provide further guidance and recipes for achieving your objectives. Thank you for reading!

Licensed under CC BY-NC-SA 4.0