Technology8 minute read

Maintain Control: A Guide to Webpack and React, Pt. 2

In a React/Webpack development scenario, there are myriad options to choose from. It’s worth exploring some advanced techniques when it comes to TypeScript, CSS, web workers, and service workers.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

In a React/Webpack development scenario, there are myriad options to choose from. It’s worth exploring some advanced techniques when it comes to TypeScript, CSS, web workers, and service workers.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Michael Pontus
Verified Expert in Engineering
7 Years of Experience

Michael is a senior full-stack developer specializing in front-end development with React and TypeScript.

Read More
Share

In the first part of this React-Webpack tutorial, we discussed how to configure loaders and perform optimization. Now, we’ll get into more advanced techniques related to specific React/Webpack config use cases.

TypeScript and React with Webpack: Enter Babel

There are several ways in which you can use TypeScript in your React project. While ts-loader is a good option, I’d like to focus on how to transpile TypeScript using @babel/preset-typescript because many libraries are publishing Babel plugins to perform compile time optimization. In addition to processing TypeScript files, it will allow us to use Babel plugins provided by various libraries such as styled-components or react-intl.

The first thing we’ll need to do is to install TypeScript and Babel dependencies:

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

We’ll then generate a TypeScript configuration file using the command-line program tsc:

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

The command above will generate a tsconfig.json suitable for writing code for a browser environment. The --isolatedModules option enforces some constraints which make sure that the code you write will be compatible with @babel/plugin-transform-typescript. This option is useful to have in order for your IDE to warn you when you are writing code in a way that Babel will not be able to transform.

Next, we are going to update babel.config.js by introducing a new preset:

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

And enable the .ts file extension in webpack.config.js:

@@ -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({

The above configuration is enough to be able to transpile our code but it does not actually validate it. We will need to perform type checking in a separate, parallel process using fork-ts-checker-webpack-plugin.

First, we need to install it:

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

Then, we’ll add it to the plugins section in webpack.config.js:

@@ -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: {

Specifying async: false will prevent Webpack from emitting invalid code and show compilation errors in an overlay when running a development server.

Note: You may also be interested in Babel macros, which are gaining traction.

CSS, Enhanced via Webpack

In the previous article, we covered basic styling using css-loader. There are several ways in which we can improve on this configuration.

The proposed configuration will take advantage of CSS Modules, Sass, and PostCSS technologies. Although they do complement each other in some ways, you don’t need to use all of them at the same time. The final setup will have all of the above plugins enabled, and we’ll leave it up to you to leave something out if you are certain “you ain’t gonna need it.”

CSS Modules

CSS Modules address the problem of global scoping in CSS files by generating a randomized, unique name for each CSS class. From the point of view of a JavaScript file consuming a CSS Module, an association between the original class name and the randomized one is represented by an object exported by the loader. It lets you find and use classes specified in a CSS file in a way that makes accidental collision almost impossible.

CSS Modules support is already included in css-loader. Now we are going to need to add a new rule to make it explicit as to when CSS Modules are in use:

@@ -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: {

With that, any file ending in .module.css will be processed with CSS Modules enabled.

PostCSS

PostCSS is an extensible CSS processing framework with a huge library of plugins that you can use to extend CSS syntax, perform optimization, or provide fallbacks for older browsers.

First, we are going to install the necessary dependencies:

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

And update our CSS configuration:

@@ -47,9 +47,11 @@ module.exports = function(_env, argv) {
             {
               loader: "css-loader",
               options: {
-                modules: true
+                modules: true,
+                importLoaders: 1
               }
-            }
+            },
+            "postcss-loader"
           ]
         },
         {

We are going to configure PostCSS using the following plugins:

  • postcss-import: Enables PostCSS to process @import statements
  • postcss-preset-env: Applies polyfills to support modern CSS features in most browsers

Create a file called postcss.config.js and populate it with the following:

module.exports = {
  plugins: {
    "postcss-import": {},
    "postcss-preset-env": {}
  }
};

You can check out the PostCSS plugin directory for other extensions that you might find useful and add them to your config.

Sass/SCSS

Sass is another popular CSS processing framework. Unlike PostCSS, Sass comes with “batteries included.” Out of the box, Sass offers support for nested rules, mixins, and rewriting rules for backward compatibility. While PostCSS aims to preserve standard CSS syntax, Sass syntax may diverge from the CSS spec. Despite this, Sass is such a ubiquitous solution that using it for authoring CSS may just be an easier option—but one that depends on your requirements.

First, we are going to install the necessary dependencies:

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

Then, add a new loader to our Webpack configuration:

@@ -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: {

We preemptively addressed a couple of issues with the above snippet:

  1. We introduced resolve-url-loader after sass-loader to make relative imports work from @imported Sass files.

  2. We specified importLoaders option for css-loader to process @import-ed files using the loaders that follow it.

With the above configuration, we can start authoring our styles using Sass/SCSS in addition to PostCSS and CSS Modules which we described before. Although all of these options can be enabled simultaneously, you don’t have to use them all inside the same project, so you can choose the one tool that best fits your requirements.

Web Workers

Web workers is a powerful concept of the modern web. It lets you offload expensive computations away from the main thread. Web workers should be used sparingly and reserved for things that can’t be otherwise optimized by intelligent scheduling inside an event loop. Using web workers is a good candidate for optimizing long, synchronous operations.

Webpack makes it easy to use web workers with worker-loader, which bundles worker files into the output directory and provides a worker class to the consumer file.

First, we need to install worker-loader:

npm install -D worker-loader

Then add it to our configuration file:

@@ -31,6 +31,10 @@ module.exports = function(_env, argv) {
             }
           }
         },
+        {
+          test: /\.worker\.js$/,
+          loader: "worker-loader"
+        },
         {
           test: /\.css$/,
           use: [

Now, all you need to do to start using web workers is to instantiate a class imported from a file ending in .worker.js that implements the ordinary Worker API.

Service Workers

Service workers enable advanced optimization techniques and improvements to user experience. They let your app work offline when a user loses their network connection. They also let your app load instantaneously even after pushing an update.

Webpack makes it easy to configure service workers for your app using the workbox-webpack-plugin module. First, we need to install it:

npm install -D workbox-webpack-plugin

Then, we’ll add the plugin to the plugins section of our Webpack configuration:

@@ -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: {

The above configuration uses the following options:

  • swDest specifies the output filename for the generated worker file.
  • clientsClaim instructs the service worker to take control of the page immediately after registration and begin serving cached resources instead of waiting for the next page reload.
  • skipWaiting makes updates to the service worker take effect immediately instead of waiting for all active instances to be destroyed.

There’s a good reason why the two latter options are not the default. When enabled simultaneously, there is a potential for glitches to occur in time-sensitive situations, so it’s up to you to make a conscious decision on whether to keep those options enabled in your config.

Finally, we need to register the service worker when a user opens our app:

@@ -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 are capable of a lot more than adding offline capabilities to our app. If you need a greater degree of control over service worker behavior, then you might use the InjectManifest plugin instead. By writing your own service worker file, you can also enable caching for API requests and use other features enabled by service workers such as push notifications. You can find out more about Workbox’s capabilities in the Advanced Recipes section of its official documentation.

Advanced React Webpack Config: Giving Your Project an Edge

This second part of our Webpack tutorial series should have armed you with the necessary knowledge to extend your Webpack configuration past the most general React use cases. I hope you have found this information useful and that you can confidently extend your personalized configuration to achieve the goals specific to your project.

As always, you can find the complete configuration files on GitHub and refer to the Webpack documentation and its plugins section to find more recipes applicable to your goals. Thank you for reading!

Understanding the basics

  • What does the babel-loader package do?

    Babel-loader enables processing of imported JavaScript files using Babel and its various plugins.

  • What are Webpack loaders?

    In Webpack, loaders specify the rules for processing imported files.

  • Who created Webpack?

    The Webpack project was founded by Tobias Koppers in March 2012 and continues to be developed to this date by many Webpack maintainers and contributors.

  • What is Webpack config?

    Webpack config is a JavaScript object which specifies the rules for processing input files and creating output bundles.

  • Why is Webpack used?

    Webpack is used to transpile modern JavaScript code into bundles that can be executed inside a browser.

Hire a Toptal expert on this topic.
Hire Now
Michael Pontus

Michael Pontus

Verified Expert in Engineering
7 Years of Experience

Porto, Portugal

Member since December 5, 2018

About the author

Michael is a senior full-stack developer specializing in front-end development with React and TypeScript.

Read More
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.