(This tutorial is written using webpack v5 however the general concepts will apply to any version)
Table of Contents
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
src/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Webpack Example</title>
</head>
<body></body>
</html>
src/script.js
const element = document.createElement("h1");
element.innerHTML = "Welcome";
document.body.appendChild(element);
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",
}),
],
};
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
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);
Run npx webpack
and take a look at our "error" program. Serve the dist
directory and look at the browser's development console.
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",
...
};
Now when we run npx webpack
and look at the console:
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);
Next we add a devServer
property to our webpack config:
webpack.config.js
...
module.exports = {
...
devServer: {
static: './dist',
},
...
};
Then we install DevServer:
npm install --save-dev webpack-dev-server
Finally we run the command:
npx webpack serve --open
By default webpack will serve your app on port 8080. The --open
flag will automatically open your browser to the page for you.
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
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);
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>
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
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',
},
}
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 thanJS
. 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 toimport 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 seeprocess 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
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
(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);
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
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",
...
}
}
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.
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.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
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
],
},
...
};
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';
...
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'
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
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" },
],
},
...
};
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:
- Understanding the Modern Web Stack: Running a Local Web Server
- Understanding the Modern Web Stack: Babel
- Understanding the Modern Web Stack: Webpack - The Basics
- Understanding the Modern Web Stack: Webpack - Loaders, Optimizations & Bundle Analysis
- Understanding the Modern Web Stack: Webpack - DevServer, React & Typescript
@eagleson_alex on Twitter
Thanks for reading, and stay tuned!
Top comments (4)
To build using
"jsx": "preserve"
you need yourmodule
configuration inwebpack.config.js
to look something like below. The key is having all the necessary loaders in theuse
array, in reverse order (as mentioned in the article), not having a separate rule forts-loader
as in the article. Otherwise they will not chain as expected. See webpack docs.HTH
Thank you @mnemotic
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'],
},
"jsx": "react-jsx" fixed my problem. Thanks!