How to set up a React app without using Create React App (CRA)

How to set up a React app without using Create React App (CRA)

Introduction

I know most React developers are quite familiar with the create-react-app command line(CLI) command for setting up a react boilerplate. Well, in this article, I will discuss another way of setting up a react app. The aim of this post is to show you how we can configure webpack to handle a React application. To follow along; you need a little or no knowledge in webpack, I will make it as clear as possible and also, a basic understanding of React, as that's what this post is about.

What is Webpack?

Webpack is simply a module bundler for modern Javascript application. Why the word “modern”? The word “modern” here means new principles and frameworks e.g. ES6, React, Angular etc. With the pace at which Javascript is evolving, it has been quite difficult for the browser to keep up with it. Webpack serves as a breach or more like a connector, it basically compiles the “modern” Javascript back to the version the browser understands. For more understanding of webpack, visit their official site here.

What is React?

React is a Javascript library for building client-side web applications. For more understanding of React, visit their site here.

Project Structure

Let's create the required files needed for our project.

  • Create a project folder custom-react-boilerplate.
  • Initialize npm in the folder, by running this command on the CLI; npm init, and follow the steps to set up a new node project.
  • Create a folder name public and in the folder, create an index.html file.
  • Create another folder called src and create these two files; App.jsx and App.scss.
  • Create .babelrc file.
  • Create webpack.config.js file.
  • Install react and react-dom via the CLI by running npm install react react-dom.

Your project structure should be exactly as what we have below;

:custom-react-boilerplate
   - node_modules
   - public
      - index.html
   - src
     - App.jsx
     - App.scss
   - .babelrc
   - package.json
   - webpack.config.js

node_modules:

All installed libraries are stored here.

public- index.html:

This is the template where we will mount the bundled javascript code by targeting an id. Let's create a simple HTML page containing a div of id="root" as shown below.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Custom React Boiler Plate</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

src- App.jsx: Entry point of our react application.

This is the entry point of our react application where our react components will live in. Let have a simple count up app for testing purpose as shown below:

 import React, { useState } from 'react';
import ReactDom from 'react-dom';
import './App.scss';

const Test = () => {
    const [count, setCount] = useState(0);

    const increment = () => {
        setCount(prevState => prevState + 1);
    }
    return (
        <div>
            <p>{count}</p>
            <button onClick={increment}>INCREMENT</button>
        </div>
    )
}

let root = document.getElementById('root');
root && ReactDom.render(<Test />, root);

From the code snippet above, we are mounting the component in the index.html page we created earlier on via the div tag of id root.

App.scss:

All SCSS and CSS entry point (optional).

.babelrc

Babel configurations for react application will live here. Babel enables support for ES6 and JSX i.e it compiles it down to ES5 (which the browser understands). Install the dependencies to set up babel configuration by running this command on the CLI npm install babel-loader @babel/core @babel/preset-react @babel/preset-env --save-dev. With that being installed, add the necessary configuration to the .babelrc file to enable babel as shown below:

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

With all these in place, let's dive into the real deal, the webpack configuration proper.

webpack.config.js

This is the key to our application. Here, we will configure webpack for compiling and loading our assets. Before we begin, let us install the following; webpack, webpack-cli (This will enable us to set up custom webpack configuration based on our use-cases and needs.) and webpack-dev-server (This compiles the codes and keeps in memory for development purposes and also will use this to set up a local server). Using this command, let's install the necessary dependencies; npm install webpack webpack-cli webpack-dev-server --save-dev.

To get started with webpack configuration proper, let's look at the core concepts for the configuration which are listed below;

  • Entry
  • Output
  • Loaders
  • Plugins

Entry

This specifies the entry point for webpack, webpack looks out for this configuration to know where to compile from, let's point to our `App.jsx' file as shown below;

module.exports = {
    entry: './src/App.jsx'
};

Output

The output property tells webpack where to dump the bundle after compiling and the name of the file it saves to.

const path = require('path');

module.exports = {
    entry: './src/App.jsx',
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.js'
    }
};

In the above, we simply told webpack to save our file in a build folder using output.path and specified the name of the file bundle.js it saves to using output.filename.

Loaders

Why do we need loaders? Webpack supports javascript and JSON files by default, loaders are needed to tell webpack to parse other kinds of files depending on the loader passed. Here, we will need sass-loader; (compiles sass to CSS), CSS-loader; ( Translates CSS into CommonJS) and style-loader (Creates style nodes from JS strings). Let's install these loaders using npm install node-sass sass-loader CSS-loader style-loader --save-dev. After which that has been done, we specify the loaders rules in webpack.config.js as shown below;

const path = require('path');

module.exports = {
    entry: './src/App.jsx',
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: "babel-loader"
            },
            { 
                test: /\.(css|scss)$/,
                exclude: /node_modules/,
                use: [
                    "style-loader",
                    "css-loader",
                    "sass-loader"
                ]
            }
        ]
    }
}

From the above, we specified two loaders rules; the babel-loader for parsing all js and JSX based on the regular expression passed in the 'test' key and also we passed sass, CSS and style loaders to load every file with a CSS or scss extension excluding all those in the node_modules.

Plugins

Plugins are used to perform a wider range of tasks like bundle optimization, asset management and injection of environment variables. To use a plugin, you need to install it, if it doesn't exist and require() it before initializing in the plugins array in webpack configuration. Let's install html-webpack-plugin, this generates an HTML file for the application and also injects the compiled bundle into it without you doing any work. Install using npm install html-webpack-plugin --save-dev and configure as shown below;

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

module.exports = {
    entry: './src/App.jsx',
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: "babel-loader"
            },
            { 
                test: /\.(css|scss)$/,
                exclude: /node_modules/,
                use: [
                    "style-loader",
                    "css-loader",
                    "sass-loader"
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({template: './public/index.html'})
    ]
}

After importing the plugin installed above, we included and initialized it in the plugins array using new(), we also specified a base template which we had created earlier on as a placeholder for mounting the compiled javascript. With the above being done, we have webpack configuration set up and ready to run our React application. Let's go over to set up the npm command to run webpack in package.json file as shown below;

 "scripts": {
    "start": "webpack-dev-server --port 5000 --open",
    "build": "webpack --mode production"
  }

From the above, we set up two npm CLI commands; start and build. On npm start, we are instructing webpack-dev-server to compile and keep in memory and also open up a local serve at port 5000 while on npm run build, to run webpack for production use i.e a more compiled and optimized version than the previous. Now, head over to your terminal and run npm start and watch the magic happen. There we go with our custom react boilerplate.

NOTE: Only use npm run build on production (bundle would live in a build folder after compiling is done).

BONUS TIPS

So far we've gotten it to work as planned, let's do a little cleanup.

Hashing

While working on a production environment, there's quite a lot that goes on, one being caching that happens on the browser. Browser caching stores web page resource files on a local computer. In most cases, when a user visits a web page already visited before and there's no change in the file name of the assets, it tends to use that which are saved in the cache. This causes new changes made in a file not to take effect.

To eliminate this from happening with the bundle.js file, we need to introduce a hash to the naming convention of the bundle, this hashing makes sure the name of our bundle.js file isn't static and that it changes whenever there's a change in its content. Webpack supports hashing out of the box. So let's apply this in webpack.config.js by simply passing this variable [hash] as part of the name of the bundle file and that's all that needs to be done, no extra configuration is needed.

Clean Bundles

Also, we need to install another plugin for cleaning up unused builds/bundles by running this command on the CLI; npm install clean-webpack-plugin --save-dev.

Let's configure all the above in webpack as shown below;

const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    output: {
        filename: 'bundle.[hash].js'
    },
    plugins: [
       new CleanWebpackPlugin()
    ]
}

From the above where we have the [hash], that would be replaced with some random string. The clean-webpack-plugin was also required and initialized in the plugins array.

Extract CSS

In addition, let's also extract the CSS into its own file, for a cleaner bundle. Install a plugin for that by running this command; npm install mini-css-extract-plugin --save-dev, we will replace the style-loader with this. We aren't injecting the styles directly on the HTML page via the <style> tag any longer, but rather we are moving the styles to its own CSS file and linking it in the template. All these would be done by the plugin, so don't sweat it.

Let's configure as shown below;

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
        rules: [
            { 
                test: /\.(css|scss)$/,
                exclude: /node_modules/,
                use: [
                    MiniCssExtractPlugin.loader,
                    "css-loader",
                    "sass-loader"
                ]
            }
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'main.[hash].css'
        }), // Extract css to a file
    ]
}

From the above, the style-loader was replaced with the mini-css-extract-plugin and the plugin was initialized in the plugin array, passing in the name of the file to extract to, we also hashed to take care of caching.

With all these being done, the final webpack configuration should be the same with what we have below;

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    entry: './src/App.jsx',
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'bundle.[hash].js'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: "babel-loader"
            },
            { 
                test: /\.(css|scss)$/,
                exclude: /node_modules/,
                use: [
                    // "style-loader",
                    MiniCssExtractPlugin.loader,
                    "css-loader",
                    "sass-loader"
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(), // Clean builds not in use
        new MiniCssExtractPlugin({
            filename: 'main.[hash].css'
        }), // SExtract css to a file
        new HtmlWebpackPlugin({template: './public/index.html'})
    ]
}

Conclusion

In conclusion, understanding this method of creating a react app is a great way to get to know how webpack is configured to handle a react application and is not a replacement for CRA. Thank you for taking your time to go through this, leave a like or comment if you find this useful.

Check out the final code on Github.