DEV Community

Alex Eagleson
Alex Eagleson

Posted on

Understanding the Modern Web Stack: Webpack - DevServer, React & Typescript

(This tutorial is written using webpack v5 however the general concepts will apply to any version)

Table of Contents

  1. Recap
  2. Initializing the Project
  3. Source Maps
  4. Webpack DevServer
  5. React and JSX
  6. Typescript
  7. Wrapping Up

Recap

This tutorial presumes you are already familiar with the basics of webpack outlined in this tutorial:

Understanding the Modern Web Stack: Webpack - The Basics

We demonstrated how we can importing individual files and functions from larger libraries without the need to carry along code we aren't using.

This tutorial introduces you to source maps, webpack's DevServer, as well as how to introduce both React and Typescript into a webpack project.

So before we dive in, let's get our working environment set up.

Initializing the Project

Create the following directory structure:

root
|   webpack.config.js
└───src
│   │   script.js
|   |   index.html
Enter fullscreen mode Exit fullscreen mode

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Webpack Example</title>
  </head>
  <body></body>
</html>
Enter fullscreen mode Exit fullscreen mode

src/script.js

const element = document.createElement("h1");

element.innerHTML = "Welcome";

document.body.appendChild(element);
Enter fullscreen mode Exit fullscreen mode

webpack.config.js

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

module.exports = {
  mode: "none",
  entry: "./src/script.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: "./src/index.html",
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Now install your dependencies, and run the webpack command to generate your dist folder.

npm init -y
npm install webpack webpack-cli html-webpack-plugin --save dev
npx webpack
Enter fullscreen mode Exit fullscreen mode

If you can successfully serve your dist folder and see the Welcome message then you're ready to start the tutorial.

Source Maps

A source map is a type of file (or annotations within a file) that provides information to other tools about the origin of source code before transformations occurred.

For example if we run webpack on even a single line .js file, the bundled version will include a small amount of additional code injected by webpack. This means that the code you wrote on line 1 may actually appear on line 10 or line 10000 of your bundle.

This makes it hard to locate errors when your browser reports the error on a different line based on the bundle it's running, rather than the original line in your source code.

Source maps solve this problem for us. Let's demonstrate how.

First we will add an error to our script.js file:

src/script.js

throw Error("Something happened!");

const element = document.createElement("h1");

element.innerHTML = "Welcome";

document.body.appendChild(element);
Enter fullscreen mode Exit fullscreen mode

Run npx webpack and take a look at our "error" program. Serve the dist directory and look at the browser's development console.

Without Source Maps

Notice that webpack is reporting the error on line 3 of main.js (your line number could be different).

This is technically correct since that's where webpack has placed the throw Error code on the output main.js file.

To configure it to report the correct line numbers add the following line to your webpack config:

webpack.config.js

...
module.exports = {
  ...
  devtool: "inline-source-map",
  ...
};
Enter fullscreen mode Exit fullscreen mode

Now when we run npx webpack and look at the console:

With Source Maps

The error is now correctly being reported where it is actually located in our code!

Webpack DevServer

webpack's DevServer is an amazing tool to speed up your development time by giving you instant reloading and instant feedback to your changes.

DevServer will automatically watch your files for changes and update your bundle automatically when you save. While using live server the bundle lives in memory (RAM) rather than the dist folder so it is able to update much faster.

Let's configure DevServer and give it a try. We'll start by creating a JS file we can test. Remove the throw Error line and update script.js to match the following:

src/script.js

const element = document.createElement("h1");

element.innerHTML = "Welcome";

document.body.appendChild(element);
Enter fullscreen mode Exit fullscreen mode

Next we add a devServer property to our webpack config:

webpack.config.js

...
module.exports = {
  ...
  devServer: {
    static: './dist',
  },
  ...
};
Enter fullscreen mode Exit fullscreen mode

Then we install DevServer:

npm install --save-dev webpack-dev-server
Enter fullscreen mode Exit fullscreen mode

Finally we run the command:

npx webpack serve --open
Enter fullscreen mode Exit fullscreen mode

By default webpack will serve your app on port 8080. The --open flag will automatically open your browser to the page for you.

Webpack DevServer Example

Try changing the Welcome string to anything else in script.js and you'll see that the page updates immediately. This creates an extremely smooth development environment for you with instant feedback.

Next we will look at how to introduce React and JSX.

React and JSX

(This tutorial uses React v17, though concepts are the same for any version)

This section of the tutorial assumes you are already familiar with the basics of React and JSX. If you need a refresher the React documentation is the best place to start.

Our goal in this section will be to show you how to configure webpack to transform your JSX into regular Javascript code. webpack relies on a loader for Babel to accomplish this. If you're not familiar with how Babel works, this tutorial will cover everything you need to know:

Understanding the Modern Web Stack: Babel

Our first step will be to install React and update our script.js file to use create a React component. Begin by installing React with this command:

npm install react react-dom --save-dev
Enter fullscreen mode Exit fullscreen mode

Next we update our script.js file to use JSX. Since JSX is a special syntax on top of Javascript, and not valid Javascript on its own, it requires a different file extension.

Delete script.js and create script.jsx in the same location with this code:

src/script.jsx

import ReactDOM from 'react-dom';

const Welcome = () => {
  return <h1>Welcome</h1>;
};

const mountingNode = document.querySelector("#root");

ReactDOM.render(<Welcome />, mountingNode);
Enter fullscreen mode Exit fullscreen mode

If you are familiar with React you can see this mounts our component onto a root node with an id="root". Update our index.html to include it:

src/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Webpack Example</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

We also need to install the packages that will run the transformations for us:

npm install babel-loader @babel/core @babel/preset-env @babel/preset-react --save-dev
Enter fullscreen mode Exit fullscreen mode

In the above list you can see all the standard pieces of Babel that we learned about in the Babel tutorial.

The only new one is babel-loader which is the loader for webpack which runs babel as part of the bundling process.

Now let's update our webpack config to use babel loader:

webpack.config.js

...
module.exports = {
  entry: "./src/script.jsx",
  ...
  module: {
    rules: [
      {
        test: /\.m?jsx$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              "@babel/preset-env",
              ["@babel/preset-react", { runtime: "automatic" }],
            ],
          },
        },
      },
    ],
  },
  optimization: {
    nodeEnv: 'development',
  },
}
Enter fullscreen mode Exit fullscreen mode

There's a few things to unpack here so let's do so one at a time:

  • entry: Has been updated to use the JSX extension rather than JS. Don't miss this small change as it is a common source of errors.

  • rules: We have a regular expression to ensure babel loader is run on any JSX files in the project. If we are using React v17 or later then we need the { runtime: "automatic" } which tells Babel to include the JSX runtime in our bundle so we don't need to import React in our JSX code.

  • optimization: This is another key source of errors. React requires process.env.NODE_ENV to be defined (set to either development or production) similar to webpack. We will set it to development for now. If you see process is undefined error it likely means you forgot this value.

Now run either the npx webpack serve --open command (or npx webpack if you want to output to disk and serve it yourself). If everything has worked smoothly you'll see your Welcome page again.

Congratulations! You've now implemented React and JSX support into a webpack configuration.

Our final section will show how to implement Typescript.

Typescript

(This tutorial uses Typescript v4.4, though concepts are the same for any version)

This section of the tutorial assumes you are already familiar with the basics of Typescript. If you need a refresher the Typescript documentation is the best place to start.

It can often be tricky and confusing for newcomers to understand how to integrate Typescript into a webpack configuration, particularly if you are also using JSX and React.

It's the reason tools like Create React App are so popular, because they handle all that configuration for you. But that can be problematic when you need to configure something, and that's why this tutorial exists.

Typescript, webpack and babel can sometimes overlap in the features they offer. In this tutorial we will take a look at each one with the aim of understanding what they are doing, so you will have a better understanding of the best way to manage them yourself.

We'll begin by installing Typescript:

npm install typescript --save-dev
Enter fullscreen mode Exit fullscreen mode

After we have installed Typescript, we need to initialize it within our project. That will automatically create a tsconfig.json file where we can configure Typescript options similar to our webpack config:

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

(Be careful with this syntax, notice we are using the npx package runner like we do with webpack. tsc is the name of the command line program for Typescript)

Next let's add some Typescript syntax to our script.jsx file. Similar to the transition to JSX, Typescript also requires its own format to indicate a file is using Typescript.

The base extension for Typescript files is .ts, however if you are using JSX, the extension is .tsx. Let's update our file extension and add some code:

script.tsx

import ReactDOM from 'react-dom';

const x: number = 5; // <-- NEW

const Welcome = () => {
  return <h1>Welcome</h1>;
};

const mountingNode = document.querySelector("#root");

ReactDOM.render(<Welcome />, mountingNode);
Enter fullscreen mode Exit fullscreen mode

If using an IDE like VS Code you may notice a couple of errors being highlighted. The first is the ReactDOM does not include types. The second errors will highlight your JSX syntax. That's because Typescript is not configured to handle JSX by default, we have to configure it for that.

We'll begin by providing type info for ReactDOM.

Libraries and packages that don't come bundled with their types automatically often have type packages available in Definitely Typed.

So to import those types from that library for ReactDOM we run the following command:

npm install @types/react-dom --save-dev
Enter fullscreen mode Exit fullscreen mode

This will fix out missing types for ReactDOM. Next let's configure Typescript to handle JSX.

Understanding the all the options in tsconfig.json is beyond the scope of this tutorial, but we are spoiled now since they have implemented a system of automatically generated comments within the generated file. If that isn't enough you can find full tsconfig documentation here.

We need to set the jsx property in our tsconfig file. The default value is preserve. What this means is that Typescript will completely ignore the JSX. It will output a .jsx file instead of a .js file and leave it to you to use another tool to remove the JSX.

Let's test that out. Update your tsconfig file:

tsconfig.json

{
  "compilerOptions": {
    ...
    "jsx": "preserve",
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Then run npx tsc. You will see in your src directory that a script.jsx file is generated. This is your script.tsx file with the types checked and remove. Great! We're making good progress.

Feel free to experiment with different settings of jsx. For example react will remove your JSX for you and output a .js file, but it will presume you are importing React. A value of react-jsx will use the new JSX runtime in React v17 so you don't need to import.

It makes little difference (as far as I know) which tool you use to transform JSX. We will leave it as preserve for Babel since we already configured Babel to handle JSX in the previous section.

If there is a good reason to choose one over the other, feel free to let us know in the comments!

At this point you can remove any script.js or script.jsx files you generated while testing. We only need our script.tsx source file.

We have two options for adding a Typescript stage to our webpack process.

  1. We can use ts-loader which will perform type checking during the bundling process. If there are any type errors, the build will cancel and report the error.

  2. We can use Babel to simply remove the type info. This presumes we are using another tool to type check before bundling. It will not cancel the build process on errors.

We'll look at how you can configure both options and choose the one that is right for you.

Option 1: ts-loader

This is the option that will do the type checking and type removal. We need to install the loader:

npm install ts-loader --save-dev
Enter fullscreen mode Exit fullscreen mode

Then we update out webpack config to include the following line:

webpack.config.js

...
module.exports = {
  entry: "./src/script.tsx", // <-- NEW
  ...
  module: {
    rules: [
      {
        test: /\.m?jsx$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              "@babel/preset-env",
              ["@babel/preset-react", { runtime: "automatic" }],
            ],
          },
        },
      },
      { test: /\.tsx?$/, use: "ts-loader" }, // <-- NEW
    ],
  },
  ...
};
Enter fullscreen mode Exit fullscreen mode

We have two new lines here. The first is an update to the entry point. We need to target our script.tsx file now instead of .jsx.

The second is the ts-loader. Loaders happen in a chain and execute in reverse order. So we need to put our Typescript loader at the end so that it passes the transformed JSX file down the chain to babel-loader.

Run npx webpack serve --open and see the result. If all goes well you're see your welcome page with type checking occurring.

To see the type checking in action, try introducing an error:

script.tsx

...
const x: number = 'this is a string';
...
Enter fullscreen mode Exit fullscreen mode

If running DevServer with watch enabled you will immediately see an error appear in both your browser and your terminal:

Type 'string' is not assignable to type 'number'
Enter fullscreen mode Exit fullscreen mode

Option 2: babel/preset-typescript

The second option assumes that we are running our own type checking before the webpack build process. If that is the case then running it a second time with ts-loader is unnecessary overhead.

Babel has a plugin to simply remove types without checking. Run the following command to install it:

npm install @babel/preset-typescript --save-dev
Enter fullscreen mode Exit fullscreen mode

Next we update our webpack config. If you following the previous step then make sure you remove ts-loader:

webpack.config.js

...
module.exports = {
  entry: "./src/script.tsx",
  ...
  module: {
    rules: [
      {
        test: /\.m?[jt]sx$/, // <-- NEW
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              "@babel/preset-env",
              ["@babel/preset-react", { runtime: "automatic" }],
              "@babel/preset-typescript", // <-- NEW
            ],
          },
        },
      },
      // { test: /\.tsx?$/, use: "ts-loader" },
    ],
  },
  ...
};
Enter fullscreen mode Exit fullscreen mode

There are two new lines to note. The first is an update to our test regex. We want babel-loader to now run on files with either JSX or TSX extension. The [jt] is part of regular expression syntax meaning "j or t".

The second is the addition of @babel/preset-typescript at the end of the presets array. Babel, like webpack, runs these presets in reverse order. We want to strip off the types first before we process the JSX.

When we run npx webpack serve --open we should once again see our big "Welcome" message on our page. If we introduce type errors webpack will still compile, so it's relying on us to do type checking as a separate process first.

Wrapping Up

Hopefully this tutorial has given you a better understanding of the complicated ecosystem of transforming JSX and Typescript code, as well as the benefits of running a local dev server that supports instant reloading.

While these tools are difficult to set up, they do provide an extremely rich and user-friendly working environment for developing complex web applications at scale. Once you learn how to configure them yourself, you will be in a much stronger position to troubleshoot any issues you encounter in your build tooling in the future.

Please check out the other entries in this series! Feel free to leave a comment or question and share with others if you find any of them helpful:

@eagleson_alex on Twitter

Thanks for reading, and stay tuned!

Discussion (3)

Collapse
mnemotic profile image
Martin Green

To build using "jsx": "preserve" you need your module configuration in webpack.config.js to look something like below. The key is having all the necessary loaders in the use array, in reverse order (as mentioned in the article), not having a separate rule for ts-loader as in the article. Otherwise they will not chain as expected. See webpack docs.

HTH

  module: {
    rules: [
      {
        test: /\.tsx$/i,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
                ['@babel/preset-react', { runtime: 'automatic' }],
              ],
            },
          },
          { loader: 'ts-loader' },
        ],
      },
    ],
  },
Enter fullscreen mode Exit fullscreen mode
Collapse
deanhiller profile image
Dean Hiller

GREAT article that helped me get setup and the only things that did not work is I had to have "jsx": "react-jsx" instead of "preserve" or my index.tsx failed in a real react app. The other change I had to have was an addition to webpack

resolve: {
extensions: ['.js', '.jsx', '.tsx', '.ts', '.html', '.scss'],
modules: ['node_modules'],
},

Collapse
prikalel profile image
Velikiy Prikalel

"jsx": "react-jsx" fixed my problem. Thanks!