If you are using React, you are probably also using create-react-app, which is a great way for setting up a new React project in a fast and easy manner. It hides away all the build process config, so you can focus on writing code immediately. This way you don't have to worry about configuring Webpack, Babel, and the other build tools. But doing this manually can be very beneficial for learning purposes, so let's see a simple way to set up your project manually.
Project initialization
Let's create a directory and initialize npm and git.
mkdir react-app
cd react-app
npm init
git init .
Our folder structure will look like this:
REACT-APP
└───src
│ └───App.js
│ └───index.js
| └───index.html
└───package-lock.json
└───package.json
└───webpack.config.json
So we will create all the needed files and directories.
mkdir src
cd src
touch App.js
touch index.html
touch index.js
Then, we need to install React runtime dependencies.
npm install react react-dom
React application setup
We will add content to the files in the src
folder, so we have a working React application.
index.html
<!DOCTYPE html>
<html>
<head>
<title>React with Webpack</title>
</head>
<body>
<div id="app"></div>
<script src="index.js" />
</body>
</html>
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("app"));
App.js
import React from "react";
export default function App() {
return <h1>Hello World</h1>;
}
If you openindex.html
in the browser, it will be blank. Reason for this is that in App.js
file we are using JSX when we write: return <h1>Hello World</h1>;
. The browser does not understand this syntax, so it needs to be transformed from JSX code into regular JavaScript. For this purpose, we use the Babel compiler.
Babel setup
First, we will install Babel core and CLI packages locally.
npm install --save-dev @babel/core @babel/cli
Use React preset
We also need to install and configure Babel to use presets, which will enable transforms for React. Let's install the required preset.
npm install @babel/preset-react --save-dev
To configure Babel we will create a babel.config.json
configuration file in the project root.
touch babel.config.json
Inside the config file, we will define which presets we want to use.
{
"presets": ["@babel/preset-react"]
}
Testing the compiled code
After we run babel src -d dist
compiled code will be located inside the dist
folder. To use the compiled code, we need to reference compiled index.js
file in index.html
file. To do this we will add <script src="../dist/index.js" />
. If we examine the code compiled by Babel, we will see that JSX syntax is compiled to valid JavaScript code.
One thing worth noting is that we are using ES modules. Since modern browsers have started to support module functionality natively, our application should work out of the box. But if we open index.html
in the browser, the first problem that we will encounter is that the browser does not recognize index.js
as a module, therefore we get an error saying Uncaught SyntaxError: Cannot use import statement outside a module
. To fix this we need to include type="module"
in the <script>
element, to declare this script as a module. Our script element will look like this:
<script type="module" src="../dist/index.js" />
This should help, right? Not really. If we try to run the page again, we will encounter the second problem: the browser is complaining that the React module relative reference is not valid. This is because the browser accepts only one kind of module specifier in an import statement: a URL, which must be either fully-qualified or a path starting with /
, ./
or ../
. One possible solution would be to use the relative path to React module located in node_modules
folder. But again, we face another problem. If you check the react
folder you can see that React currently only supports UMD and CommonJS modules. At this point, we would like to find some solution that would allow us not to worry about module formats of the dependencies and how to import them. Let's see what Webpack brings to the table and what problems it is trying to solve.
Webpack setup
Webapck bundles all the required imports into one JavaScript file to be used on the client side. This is why we call it a bundler. Since all modules are contained in one namespace, it resolves all dependency and module format problems for us. Other important features, that are worth mentioning are:
Tree shaking mechanism - it can eliminate code that’s not used and imported by any other module.
Code-Splitting - it can create multiple bundles that can be dynamically loaded at runtime.
To start using Webpack, we first need to install the required packages:
npm install webpack webpack-cli --save-dev
We are installing 2 packages: main Webpack package and webpack-cli for running Webpack commands.
Next up, let's add Webpack configuration file:
touch webpack.config.js
We will start with the basic configuration:
const path = require('path');
module.exports = {
entry: path.join(__dirname, "src", "index.js"),
output: {
path:path.resolve(__dirname, "dist"),
}
}
What's happening here? First, we are defining an entry point of an application. This is the point from which Webpack starts the bundling process and builds the dependency tree. In our case, the entry point will be index.js
file. Also, we are defining the output path for the bundled file. We will use dist
folder as the output path.
Since we have the basic configuration set up, we can build the application with Webpack CLI. We can use webpack build
command, but since this is the default command, we can use only webpack
. But if we try to run this command Webpack will output something like this:
Module parse failed: Unexpected token (5:16)
You may need an appropriate loader to handle this file type, currently, no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| import App from "./App";
|
> ReactDOM.render(<App />, document.getElementById("app"));
|
Webpack is telling us that it does not recognize JSX syntax and that it needs something called loader to handle it properly. So, let's see how to do this.
Babel loader setup
Out of the box, Webpack only understands JavaScript and JSON files. Loaders allow Webpack to understand other file types. For JSX files we will use Babel loader. We have already installed and used Babel core package and presets. Now we need to install the loader.
npm install babel-loader --save-dev
Then we can modify Webpack configuration to start using Babel loader. The configuration file will look like this:
const path = require('path');
module.exports = {
entry: path.join(__dirname, "src", "index.js"),
output: {
path:path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
options: {
presets: ['@babel/preset-react']
}
},
]
}
}
Since production mode minifies the code by default, we will use development mode because of output readability. These are the explanations for some of the other properties that are used:
test
identifies which file or files should be transformedexclude
identifies which modules should be excludeduse
indicates which loader should be used to do the transformingpresets
is a list of presets that should be used
Webpack should be satisfied now and will run the build command successfully. If we take look at the output bundle, we can see that Webpack packaged our app modules and React modules in one file. Now we can use this bundle in index.html
by adding the script tag:
<script src="../dist/main.js" />
If you open the index.html
file in the browser now, you will see that **Hello World ** message is displayed. This means our application is up and running. That's sweet 😌 . Let's see some ways that we can optimize the build process.
HtmlWebpackPlugin setup
Right now, we are including the bundle in the index.html
file manually. This is enough for our app to run. But in real world applications, we could use code splitting that would produce multiple bundles, or we could even hash bundle file names for caching purposes. It would be a tedious process to include them manually in our index.html
every time the bundles are produced. So we will automate this process by using HtmlWebpackPlugin. Plugins are third party packages that can be used with Webpack to extend its functionality. In this case, we are using HtmlWebpackPlugin. First, let's install it:
npm install html-webpack-plugin --save-dev
And then modify the configuration file:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: "development",
entry: path.join(__dirname, "src", "index.js"),
output: {
path:path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ['@babel/preset-react']
}
},
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "index.html"),
}),
],
}
After running the build command, you will notice that now there is also index.html
file included in dist
folder. And the most important thing, main.js
script tag is automatically injected. This means that we can remove the <script>
tag from src/index.html
.
Development server setup
Currently, we are manually building the bundle after every change and opening the index.html
to see the effects in the browser. This of course is not the ideal solution for the development environment and it would be best if we could automatize these steps. Webpack offers a special package called webpack-dev-server that acts as a development server and supports live reloading. This way, we will be able to host our bundle and any change in the code will cause reload of our application in the browser.
The important thing to mention here is that the devserver is creating a separate JavaScript bundle and output in the memory. It will monitor the dependencies of the entry point defined in the Webpack configuration, and re-create the output when changes are detected. We will be using this output when serving the application in the development environment, and not the output that was created by Webpack CLI. First, let's install the required package:
npm install webpack-dev-server --save-dev
Next, we need to configure dev-server in the Webpack configuration file:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: "development",
entry: path.join(__dirname, "src", "index.js"),
output: {
path:path.resolve(__dirname, "dist"),
},
devServer: {
port: 9000,
open: true,
},
module: {
rules: [
{
test: /\.?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ['@babel/preset-react']
}
},
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "index.html"),
}),
],
}
This is the basic configuration that will allow us to host the application locally. First, we define the port
on which the server will run. After that, we set open
property to true
, which means that dev server will open the application in the default browser after the server had been started. We start the browser with webpack serve
command. The application will be opened in the browser and any changes in the code will appear automagically and instantly in the browser. Remember, the dev server is serving in-memory output, so even if you clear the contents of the output folder, the page will still run.
Conclusion
In this article, we have covered the basics of the Webpack ecosystem. We have seen how to initialize a React project from scratch and how to use Babel transpiler. Also, we have learned about Webpack loaders, plugins, and how to use Webpack dev server. Of course, these are just the basics, and there are a lot more concepts to learn about Webpack ecosystem. I will cover some of them in the next posts.
You can check out the example repo here.