Set up your React environment with npm, babel and webpack

A castle built of cardboard boxes, with the react logo like a rising sun in the background.

In my last tutorials, we used the handy create-react-app package to set up our React app for us. I propose that we go a little more into the details and make our own setup from scratch.

Here’s what you will learn along this process:

  • How to manage your project’s dependencies with Node.js built-in dependency manager: npm.
  • What is transpilation and how to use Babel to output code that any browser can understand.
  • How to develop your application as independent modules and bundle them with webpack before deploying.
  • How to set up live and hot reloading for your development environment with webpack dev server

To be honest, the create-react-app tool is good enough for any of the projects I have showed in my tutorials. But I believe that by building things from the ground up, you’ll get a better understanding of what is going under the hood. This should help you debug or fine-tune your own setups to better match your needs in future development.

So, let’s get our hands dirty.

Managing dependencies with npm

The first tool we are going to need is called a dependency manager (also occasionally called package manager). In the JavaScript ecosystem, npm is one of the two big players, the other one being yarn, that I don’t personally use. In fact, npm comes installed with every versions of Node.js, which it needs to operate.

The purpose of Node.js is to operate JavaScript outside of the browser, so if you have followed one of my previous tutorial you have it already installed on your machine. Otherwise you can download it from Node.js’ official web site1.

Once it is installed, we can open a terminal session from our development folder. Here we will create a new directory by issuing bash commands, and jump into it.

mkdir react-dev-setup
cd react-dev-setup

Next, we initiate the npm package for our project, with the command:

npm init

Then our terminal will ask us several questions. The only mandatory answer is the package name, but it is set by default to the folder’s name, so we can just let it empty, and just hit Enter for every other question.

If you want to skip these questions next time, use the npm init -y command instead.

The React package is like a cardboard box containing all you need to create React apps.

Let’s say you’d like to follow my tutorial about React State Hook. React Hooks were introduced as a standard feature since React 16.8. So we will use npm to make sure we have a version of React superior to 16.8:

npm install react@>=16.8

Specifying the version constraint (@>=16.8) is not mandatory, since npm will by default try to find the latest version. But I wanted to talk about version constraints so…

That would be enough to declare our first components and hooks. Although to be able to render them, we will also need the react-dom library.

npm install react-dom

You noticed that we didn’t specify the version here. Indeed, that is the purpose of a dependency management tool, such as npm. This means that for each package that we ask it to install, it will also install all the dependencies of these packages. As well as the dependencies of these dependencies, and so on…

Npm packages are like boxes that contains boxes that contains boxes...

You can see these dependencies inside the node_modules folder that npm created inside our project. You will notice that along react and react-dom, we also have the js-tokens, loose-envify, object-assign and scheduler packages installed.

Both react and react-dom depends on these same packages. You can see that by removing both packages:

npm remove react react-dom

Now your node_module folder is cleaned of every package. If you want to reinstall either the react or react-dom package, npm will install back all their dependencies. Whichever you choose.

So npm will try to choose the best version of each package you install to be compatible with the already installed dependencies.

So our version constraints for react@>=16.8 will decide which are the correct versions for js-tokens and other dependencies. And when we install react-dom, npm will pick the best version to be compatible with these dependencies that it shares with react

Phew, thankfully, this is not something we have to take care by ourselves, since npm does it for us. Thanks mate!

Now that we have installed both react and react-dom, we should be able to start coding our app, right? So let’s create a basic application:

const React = require( 'react' )
const ReactDOM = require( 'react-dom' )

const App = function() {
    return(
        <p>Hello World!</p>
    )
}

ReactDOM.render(
    <App />,
    document.getElementById('root')
)

export default App

And then we include it in a simple index.html:

<!DOCTYPE html>
<html lang="en-US">
    <head>
        <title>React Development Setup</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="device-with;initial-scale=1"/>
    </head>
    <body>
        <div id="root"></div>
        <script src="index.js"></script>
    </body>
</html>

If we open it now with our browser, we will see nothing. And opening the developer console will show us the following error message:

Uncaught SyntaxError: expected expression, got '<'

That’s because we wrote some JSX markup but opened it with our browser, that can only interpret native JavaScript or HTML, but not JSX!

So we need one more step, the compilation of our code by the React library.

Transpiling code with Babel

At its origin, JavaScript was only implemented in the Netscape web browser2. It was soon adopted by other web browsers but each browser was running its own version of JavaScript, with differences in syntax and API…

Now that JavaScript specifications are managed by the ECMA committee3, the differences in support for JavaScript by web browsers are of lesser importance, but still exists! And you cannot be sure that users of your application will not be running some outdated browser version as well…

That’s why web application that requires JavaScript to run now goes through a transpiling phase. This word is a shorthand for translation and compilation.

Indeed, transpiled code needs to be parsed and rewritten in a syntax that may be understandable by the most web browsers. In the world of JavaScript transpilers, Babel is the de facto standard.

Babel makes React code understandable by all web browsers.

What this all has to do with our React app? Well, the React framework had is own compiler for a time. But front-end developers were so used to transpile their final code with Babel anyway, that Facebook dropped the support for the React compiler, and maintain a Babel plugin instead.

So in order to use any JSX code inside a browser, we need to install Babel, as well as its plugins, as development dependencies.

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react

These development dependencies will only be available in our development environment. So they will not be needed to run our final build, after it has been transpiled into universal JavaScript.

But we still need to configure Babel. Create a .babelrc file inside your project’s root folder. Write this code inside:

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    "browsers": "> 0.25%, not dead"
                },
            }
        ],
        "@babel/react"
    ]
}

The @babel/preset-env will make sure to include polyfills4 for all the most modern JavaScript features in our transpiled code. We pass it the configuration object that specifies that we target every browser that has at least 0.25% ‘market shares’ (which means that they are used by at least 0.25% of visitors on the whole web).

The @babel/react preset will allow Babel to understand React standard functions as well as the JSX syntax, and indicate how to translate such rules. We can run the transpilation process by typing the following command:

./node_modules/.bin/babel -f .babelrc --out-file dist/index.js src/index.js

We are going to end up with an index.js file in our dist folder (that has been created by our command) that looks like this:

"use strict";

var React = require('react');

var ReactDOM = require('react-dom');

var App = function App() {
  return /*#__PURE__*/React.createElement("p", null, "Hello World!");
};

ReactDOM.render(document.getElementById('root'), /*#__PURE__*/React.createElement(App, null));

As you can see, there is no more JSX markup. Although, we still need to reference this JavaScript code from an HTML file in order to run it in the browser. Let’s be minimalist here:

<!DOCTYPE html>
<html lang="en-US">
    <head>
        <title>React Hooks - Memory Game</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="device-with;initial-scale=1"/>
    </head>
    <body>
        <div id="root"></div>
        <script src="dist/index.js"></script>
    </body>
</html>

So if we visit our index.html page, we see… nothing. In fact there is an error logged in the browser’s console which says:

Uncaught ReferenceError: require is not defined

Hum, it seems that our transpiled code is not so universal after all… What just happened is that our code use the CommonJs syntax for JavaScript modules5, but our browser can’t read that syntax.

JavaScript modules were a long awaited feature that allows to easily break large JavaScript applications into several smaller, more manageable and reusable files.

Except that, since this feature has been on hold for a while, several other systems for modules were implemented in the meantime, like the CommonJs project. Node.js has been able to read CommonJS modules syntax well before the shiny ES modules have made it into our web browsers.

While waiting for this feature, front-end developers have found a workaround solution to use modules in JavaScript applications: bundling them together. We’ll jump straight to this step, as bundled applications are the industry standards nowadays.

Bundling with webpack

JavaScript modules ease the development process of web applications by breaking them down into sub-parts, each with a more focused responsibility. But deploying an applications in pieces would not be very efficient: the web browser would have to issue one HTTP request per module, and if one fails, the user could start interacting with the application that would crash later on, because of a missing key module.

We can keep this ease of development, but without the drawback by bundling these modules together during the build process. To do that, we will use a bundling tool: webpack6.

Webpack is the construction engine that builds our application from all of our modules.

Just as any development tool, we can install it with npm:

npm install --save-dev webpack webpack-cli

The second package, webpack-cli will allow us to run webpack through our terminal, like this:

./node_modules/.bin/webpack

But doing so will result in an error:

ERROR in ./src/index.js 6:8
Module parse failed: Unexpected token (6:8)
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
| const App = function() {
|     return(
>         <p>Hello World!</p>
|     )
| }

Indeed, we are still trying to load JSX code into a tool that only understand native JavaScript. We will then need to tell webpack to transpile our code by using Babel. To do so, we need a special loader so webpack can pass the JavaScript files to Babel.

npm install --save-dev babel-loader

Once it is installed, we need to configure webpack to run it. Let’s create a webpack.config.js file in the root folder of our project:

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                },
            },
        ],
    },
}

Have you noticed? We are using a module again! The module.exports syntax is part of the CommonJs specification we discussed earlier. Since webpack is a Node.js program, and don’t need a web browser to be executed, it understands this syntax.

In our export statement, the module option tells webpack how to handle modules in our built code, through a set of rules (here, a set of one):

  • The test property is a regular expression7 that will filter the files. Any filename that matches this regular expression will be processed by this rule. Here, we will apply it to every file with a .js extension.
  • The exclude property is another regular expressions that will tell webpack not to process the files that are located in a folder whose name matches the regular expressions. Here we don’t want to include every of our dependencies files inside our built project.
  • Finally, the use property will tell webpack to pass the files that matches our two criteria above to the babel-loader we installed (which will then tell Babel to transpile them)

Let’s try to run our webpack build now:

./node_modules/.bin/webpack 

Now webpack has created a new file in our dist folder called main.js. If you open the file you can see code that looks like gibberish. In fact, webpack has bundled our own code with the React library, and minified the results in order for the code to take the less characters as possible.

That’s right, part of this gibberish is the full React library, included in our built app! So now our application could run all alone, since it is packed with everything it needs… Except, that our index.html file doesn’t load dist/main.js, but dist/index.js! Although we could rectify that manually, webpack can handle it for us through the html-webpack-plugin.

npm install --save-dev html-webpack-plugin

We need to add this plugin to our webpack configuration:

const HtmlPlugin = require( 'html-webpack-plugin' )

const htmlPlugin = new HtmlPlugin({
    template: './src/index.html',
    filename: 'index.html'
})

module.exports = {
    plugins: [htmlPlugin],
    // ...

Here we are importing the webpack plugin module, and create a new instance of it. We configure this instance to load the index.html file from the src folder and output it with the name index.html in the build folder (dist by default).

So that’s how our project structure is looking at this stage:

my-project
├─┬ src
│ ├── index.js
│ └── index.html
├─┬ dist
| # Built files will be outputted here...
├─┬ node_modules
| # Contains our dependencies...
├── .babelrc
├── package.json
├── package-lock.json
── webpack.config.js

And from the src/index.html file, we don’t need to link our JavaScript files anymore, webpack will handle it for us through the html-webpack-plugin. Now, if we run our build again, and open the dist/index.html file in our browser we should see:

Hello World!

At last! We got the full build process set up. But you can notice that webpack is still complaining about something while we run it:

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.

Indeed, there is many ways that webpack could help our development process, and we will see how to make dedicated development and production builds by using this mode option.

Development build with webpack dev server

One interesting tool in the webpack ecosystem is webpack-dev-server8. This will enable your local environment to act as a simple HTTP server.

Webpack dev server also has an interesting feature called live reloading. This means that each time we make a change to our source files, webpack will detect it and automatically run the build process!

We will install this tool with npm as well:

npm install --save-dev webpack-dev-server

And we can run it with:

./node_modules/.bin/webpack serve

Webpack should output a something like:

ℹ 「wds」: Project is running at http://localhost:8080/

So we can now access our built application at the URL http://localhost:8080.

Now what happens if we make a change in our src/index.js file?

const App = function() {
    return(
        <p>Hello World! How are you?</p>
    )
}

Looking at our browser again, it seems that no changes have occurred. Unless we refresh the page, and then the page displays:

Hello World! How are you?

That’s rad! But what if we don’t want to manually refresh the page every time, could we automate that as well? I’m glad you asked, we can configure our dev server to do just that!

Note: You can stop webpack dev server anytime by typing ctrl+c in your terminal.

This is also the best time to separate our development build from production build, by creating dedicated npm scripts. We do so by opening our package.json, we should see something like:

{
  "name": "react-dev-setup",
  "version": "0.1.0",
  "description": "Setup your react development environment like a pro!",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  // ...

What does interest us here is the "scripts" line, we will replace it with two of our own scripts, one for production build, and the other for development build:

  "scripts": {
    "build": "webpack --mode production",
    "start": "webpack serve --mode development"
  },

We can run the scripts by typing:

npm run build
# OR
npm run start

And they should just work like before. But now we can modify our webpack configuration depending on the mode argument we passed. For that, we will need to extract our configuration into a function in our webpack.config.js file:

module.exports = myConfiguration

function myConfiguration(env, argv) {
    const config = {
        plugins: [htmlPlugin],
        module: {
            rules: [
                {
                    test: /\.js$/,
                    exclude: /node_modules/,
                    use: {
                        loader: 'babel-loader',
                    },
                },
            ],
        },
    }
    // ...

So we will have access to the arguments we passed to inside our npm script, like the mode argument, through this function’s argv parameter. Now we can enable hot reloading, but only for our development builds:

    // ...
    if ( argv.mode === 'development' ) {
        config.devServer = {
            hot: true
        }
    }

    return config;
}

If we now run our start script again, and modify our src/index.js file, the changes now directly displays inside our browser, without us having to touch anything but the code! If you use two (or more) screens for development, you should see quite an increase in your productivity!

Webpack dev server is like an automated factory that continually builds your project.

What did you learn ?

Learning material

Most of the information I’ve told you in this tutorial comes from the official documentation of each of these tools. But I’ve still used Roy Derks, React Projects9 initial chapter to understand the very basic needs of a React application and structure my approach. You can read this chapter for free.

Of course, the books is not just about setting up development tools (which book does that?), and will lead the reader to build various projects with a practical approach.

Whichever project you choose to tackle next, enjoy your automated development process!


  1. Node.js, Home, https://nodejs.org/en/↩︎
  2. Wikipedia, Netscape Navigator, last edited January 19, 2021. https://en.wikipedia.org/wiki/Netscape_Navigator↩︎
  3. Wikipedia, Ecma International, last edited February 13, 2021. https://en.wikipedia.org/wiki/Ecma_International↩︎
  4. Mozilla Developers Network, Web Glossary, Polyfill. https://developer.mozilla.org/en-US/docs/Glossary/Polyfill↩︎
  5. Mozilla Developers Network, JavaScript Guide, JavaScript modules. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules↩︎
  6. webpack, Home. https://webpack.js.org/↩︎
  7. Mozilla Developers Network, JavaScript Guide, Regular Expressions. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions↩︎
  8. GitHub, webpack-dev-server. https://github.com/webpack/webpack-dev-server↩︎
  9. Roy Derks, React Projects, UK: Birmingham, Packt Publishing, 2019↩︎

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.