Creating a React app with Webpack

arpitmalik832

Arpit Malik

Posted on April 22, 2024

Creating a React app with Webpack

Simple step by step walk through of setting up a React app with Webpack

Sometimes you want to get started with React but don’t want all the bloat of create-react-app or some other boilerplate. Here's a step by step walk through of how to set to setup React with just Webpack!

What the project will look like when it’s done

This guide will follow the conventions already established by create-react-app - e.g. build folder is called build, static assets are under public, etc.

At the end, this is the folder structure we’ll have


- build_utils
  - config
    - commonPaths.js
    - env.js
- public
  - index.html
- src
  - App.jsx
  - index.js
.babelrc
package.json
webpack.config.js

Enter fullscreen mode Exit fullscreen mode

We’ll go step by step and check that everything works after every stage:

  1. Basic scaffolding: create project folder and serve plain HTML
  2. Add Webpack and bundle a simple JS file
  3. Add Babel for ES6 support
  4. Add React

Without further ado, let’s get started!

Step 1: Base scaffolding

First step is to create the project folder and add a plain html file.

To create the project and initialize the package.json, run these commands:


mkdir react-webpack
cd react-webpack
npm init -y

Enter fullscreen mode Exit fullscreen mode

Then, add the main index html file - it usually sits in the public directory. So, let's do that:


mkdir public
touch public/index.html

Enter fullscreen mode Exit fullscreen mode

Tip: You can open the project in VSCode by typing code . in the project folder and manually create the public folder from there

The index.html will just contain the base HTML5 boilerplate:


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
    name="viewport"
    content="width=device-width,initial-scale=1,minimum-scale=1,user-scalable=no"
    />
    <meta
    httpEquiv="Cache-Control"
    content="no-cache, no-store, must-revalidate"
    />
    <title>React + Webpack</title>
  </head>
  <body>
    <h1>Hello React + Webpack!</h1>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

Tip: In VSCode, if you type html:5 and hit tab, VSCode will create the index.html contents for you!

Now let’s check it by just serving the HTML we just created with npm serve.


npx serve public

Enter fullscreen mode Exit fullscreen mode

And it does! If you navigate to http://localhost:3000, you should see the html we just added.

Tip: If npx serve public fails for you with Must use import to load ES Module error, check your node version with the help of node -v and make sure you're using at least Node 16 (latest LTS).

Step 2: Adding Webpack

For this section, it’s best to just follow the latest official Webpack docs.

First, install Webpack:


npm install webpack webpack-cli --save-dev

Enter fullscreen mode Exit fullscreen mode

Next, let's create a simple JS file that we can configure Webpack to bundle:


mkdir src
touch src/index.js

Enter fullscreen mode Exit fullscreen mode

We can just create a div with a hello message and add it to the document:


const helloDiv = document.createElement("div");
helloDiv.innerHTML = "Hello from Javascript!";
document.body.append(helloDiv);

Enter fullscreen mode Exit fullscreen mode

Then, we need to configure Webpack by creating a webpack.config.js file in the root of the project and commonPaths.js in build_utils/config:


// webpack.config.js

const path = require("path");

const package = require("./package.json");
const commonPaths = require("./build_utils/config/commonPaths");

const isDebug = !process.argv.includes("release");

module.exports = {
  entry: commonPaths.entryPath,
  output: {
    uniqueName: package.name,
    publicPath: "/",
    path: commonPaths.outputPath,
    filename: `${package.version}/js/[name].[chunkhash:8].js`,
    chunkFilename: `${package.version}/js/[name].[chunkhash:8].js`,
    assetModuleFilename: isDebug
      ? `images/[path][name].[contenthash:8][ext]`
      : `images/[path][contenthash:8][ext]`,
    crossOriginLoading: "anonymous",
  },
};
Enter fullscreen mode Exit fullscreen mode
// commonPaths.js

const path = require("path");

const PROJECT_ROOT = path.resolve(__dirname, "../../");

module.exports = {
  projectRootPath: PROJECT_ROOT,
  entryPath: path.join(PROJECT_ROOT, "src", "index.js"),
  outputPath: path.join(PROJECT_ROOT, "build"),
  appEntryPath: path.join(PROJECT_ROOT, "src"),
  buildUtilsPath: path.join(PROJECT_ROOT, "build_utils"),
};

Enter fullscreen mode Exit fullscreen mode

Finally, in package.json, add a new build script:


"scripts": {
    "build": "webpack"
},

Enter fullscreen mode Exit fullscreen mode

Now, let's try it out! After running npm run build, you should see a new folder was created, called build, with a main.js file in it!

Tip: Add the build folder to .gitignore to not commit it by accident. And if you haven't already, make sure to also ignore the node_modules folder.

Next, we need to move the static assets to the bundle. More specifically, we want to also include the index.html file in the build folder.

Easiest way to do this is with the HtmlWebpackPlugin.


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

Enter fullscreen mode Exit fullscreen mode

And update the webpack.config.js file:


// webpack.config.js

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

const package = require("./package.json");
const commonPaths = require("./build_utils/config/commonPaths");

const isDebug = !process.argv.includes("release");

module.exports = {
  entry: commonPaths.entryPath,
  output: {
    uniqueName: package.name,
    publicPath: "/",
    path: commonPaths.outputPath,
    filename: `${package.version}/js/[name].[chunkhash:8].js`,
    chunkFilename: `${package.version}/js/[name].[chunkhash:8].js`,
    assetModuleFilename: isDebug
      ? `images/[path][name].[contenthash:8][ext]`
      : `images/[path][contenthash:8][ext]`,
    crossOriginLoading: "anonymous",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "public/index.html",
      filename: "index.html",
    }),
  ],
};

Enter fullscreen mode Exit fullscreen mode

This will copy the file under public/index.html, copy it to the build folder and inject a link to the bundled JS file (main.js).

Let’s try it out!


npm run build
npx serve build

Enter fullscreen mode Exit fullscreen mode

And it works! You should now also see the message “Hello from Javascript” :D

Adding Webpack dev server

So far, it was ok to just use npx serve to check our app works. But in real life, it's easier to just use the webpack-dev-server, so let's add that as well.


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

Enter fullscreen mode Exit fullscreen mode

Then, configure it in the Webpack config:


// webpack.config.js

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

const package = require("./package.json");
const commonPaths = require("./build_utils/config/commonPaths");

const isDebug = !process.argv.includes("release");

const port = process.env.PORT || 3000;

module.exports = {
  entry: commonPaths.entryPath,
  output: {
    uniqueName: package.name,
    publicPath: "/",
    path: commonPaths.outputPath,
    filename: `${package.version}/js/[name].[chunkhash:8].js`,
    chunkFilename: `${package.version}/js/[name].[chunkhash:8].js`,
    assetModuleFilename: isDebug
      ? `images/[path][name].[contenthash:8][ext]`
      : `images/[path][contenthash:8][ext]`,
    crossOriginLoading: "anonymous",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "public/index.html",
      filename: "index.html",
    }),
  ],
  devServer: {
    port: port,
    static: {
      directory: commonPaths.outputPath,
    },
    historyApiFallback: {
      index: "index.html",
    },
    webSocketServer: false,
  },
};

Enter fullscreen mode Exit fullscreen mode

And, then add npm run start script to package.json and while we're there, pass in the right --mode:


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

Enter fullscreen mode Exit fullscreen mode

Finally, check that it works: run npm run start, open http://localhost:3000 and check the app still works as before.

Step 3: Adding Babel

This is useful for allowing us to use all ES6 features and having them transpiled down to JS versions that all browsers can understand. (If you want to learn more about what Babel preset-env is and why it's useful, the article by Jacob Lind is a great overview).

First, let’s install the required packages:


npm i @babel/core @babel/preset-env babel-loader --save-dev

Enter fullscreen mode Exit fullscreen mode

Next, update the Webpack config to tell it to pass the files through Babel when bundling:


// webpack.config.js

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

const package = require("./package.json");
const commonPaths = require("./build_utils/config/commonPaths");

const isDebug = !process.argv.includes("release");

const port = process.env.PORT || 3000;

module.exports = {
  entry: commonPaths.entryPath,
  output: {
    uniqueName: package.name,
    publicPath: "/",
    path: commonPaths.outputPath,
    filename: `${package.version}/js/[name].[chunkhash:8].js`,
    chunkFilename: `${package.version}/js/[name].[chunkhash:8].js`,
    assetModuleFilename: isDebug
      ? `images/[path][name].[contenthash:8][ext]`
      : `images/[path][contenthash:8][ext]`,
    crossOriginLoading: "anonymous",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "public/index.html",
      filename: "index.html",
    }),
  ],
  devServer: {
    port: port,
    static: {
      directory: commonPaths.outputPath,
    },
    historyApiFallback: {
      index: "index.html",
    },
    webSocketServer: false,
  },
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/, // exclude node_modules
        use: ["babel-loader"],
      },
    ],
  },
  resolve: {
    extensions: ["*", ".js"],
  },
};

Enter fullscreen mode Exit fullscreen mode

Then, create the Babel config file — .babelrc. This is where we configure Babel to apply the preset-env transform.


// .babelrc

{
  "presets": [
    "@babel/preset-env"
  ]
}

Enter fullscreen mode Exit fullscreen mode

Optionally, update the index.js to contain some ES6 features that wouldn't work without Babel 😀 (actually they do work in most browsers, but for example IE still doesn't support Array.fill).


// Use a feature that needs Babel to work in all browsers :)
// arrow functions + Array fill

const sayHelloManyTimes = (times) =>
  new Array(times).fill(1).map((_, i) => `Hello ${i + 1}`);

const helloDiv = document.createElement("div");
helloDiv.innerHTML = sayHelloManyTimes(10).join("<br/>");
document.body.append(helloDiv);

Enter fullscreen mode Exit fullscreen mode

Finally, let’s check everything works — run npm run start and check the app correctly runs:

Step 4: Add React

Finally, we can add React 😅

First, install it:


npm i react react-dom --save
npm i @babel/preset-react --save-dev

Enter fullscreen mode Exit fullscreen mode

Then, update the .babelrc file to also apply the preset-react transform. This is needed, among other things, to support JSX.


// .babelrc

{
  "presets": [
    "@babel/preset-env",
    [
      "@babel/preset-react",
      {
        "runtime": "automatic"
      }
    ]
  ]
}

Enter fullscreen mode Exit fullscreen mode

Tip: Specifying the preset-react runtime as automatic enables a feature that no longer requires importing React on top of every file.

Also, we need to update the Webpack config to pass jsx files through Babel as well:


// webpack.config.js

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

const package = require("./package.json");
const commonPaths = require("./build_utils/config/commonPaths");

const isDebug = !process.argv.includes("release");

const port = process.env.PORT || 3000;

module.exports = {
  entry: commonPaths.entryPath,
  output: {
    uniqueName: package.name,
    publicPath: "/",
    path: commonPaths.outputPath,
    filename: `${package.version}/js/[name].[chunkhash:8].js`,
    chunkFilename: `${package.version}/js/[name].[chunkhash:8].js`,
    assetModuleFilename: isDebug
      ? `images/[path][name].[contenthash:8][ext]`
      : `images/[path][contenthash:8][ext]`,
    crossOriginLoading: "anonymous",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "public/index.html",
      filename: "index.html",
    }),
  ],
  devServer: {
    port: port,
    static: {
      directory: commonPaths.outputPath,
    },
    historyApiFallback: {
      index: "index.html",
    },
    webSocketServer: false,
  },
   module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/, // exclude node_modules
        use: ["babel-loader"],
      },
    ],
  },
  resolve: {
    extensions: ["*", ".js", ".jsx"],
  },
};

Enter fullscreen mode Exit fullscreen mode

Next, let’s create a React component, so we can check that everything works:


// src/App.jsx

const App = () => <h1>Hello from React!</h1>;

export default App;

Enter fullscreen mode Exit fullscreen mode

And add it to the main app file:


// index.js

import { createRoot } from "react-dom/client";

import App from "./App.jsx";

const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);

Enter fullscreen mode Exit fullscreen mode

Note we’re using the new React 18 syntax with createRoot.

Finally, we also update the index.html to provide a "root" node for the app:


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta
    name="viewport"
    content="width=device-width,initial-scale=1,minimum-scale=1,user-scalable=no"
    />
    <meta
    httpEquiv="Cache-Control"
    content="no-cache, no-store, must-revalidate"
    />
    <title>React + Webpack</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

Let’s check that it works — run npm run start and you should see "Hello from React!":

Also, check there are no errors in the console.

You can also check that the app correctly runs in production, by running npm run build and then npx serve build.

That’s it!

💖 💪 🙅 🚩
arpitmalik832
Arpit Malik

Posted on April 22, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related