Tech Shaadi

Bootstrapping React app with Webpack + Babel from Scratch

Webpack is a popular module bundling system built on top of Node. js. It can handle the combination and minification of JavaScript and CSS files and other assets such as image files, fonts, etc. through the use of plugins.

Before we jump into webpack here’s the full list of features we are going to set up:

  1. Webpack v4
  2. Hot Module Replacement (HMR)
  3. Code Splitting
  4. PWA
  5. SSR (Server Side Rendering)
  6. React Setup
  7. React Router DOM
  8. Configuring Prettier and Eslint (for code formatting)
  9. Storybook
  10. CI/CD Pipelines
  11. Pre-commit hooks (Husky)
  12. Bundle Analyzing

Pre-requisites

So without wasting much time, let’s start with the configuration.

Initializing npm project

npm init

Now we will install the webpack and babel packages/plugins to configure the webpack

npm i webpack-cli @babel/core @babel/preset-env @babel/preset-react
npm i better-npm-run webpack-dev-server
npm i babel-loader file-loader postcss-loader sass-loader css-loader
npm i html-webpack-plugin extract-css-chunks-webpack-plugin clean-webpack-plugin webpack
npm i copy-webpack-plugin webpack-manifest-plugin webpack-merge webpack-bundle-analyzer
npm i workbox-webpack-plugin optimize-css-assets-webpack-plugin compression-webpack-plugin

Now let’s discuss the folder structure of our project.

Here we have flowing folders:

Also, we will have a separate webpack configuration file for each environment i.e (Development and Production) for both “client” and “server”.

The reason we have a “common” config is that since there will be some common/basic configuration that will be required in both the environment (development and Production) so to eliminate code redundancy we have webpack.common.config.js, which we will import in dev and prod config file.

So before configuring webpack first let’s understand how webpack works.

There are 3 main things webpack needs to know

  1. The starting point of your application
  2. Which type for transformations to make on a particular asset i.e how you want your .html,.js and .css and other assets files to be transformed (Loders)
  3. Where it should save the new transformed code.

Also other required plugins

Let’s setup babel.config.js / .babelrc in root directory.

babel.config.js

module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
esmodules: true,
},
},
],
'@babel/preset-react',
],
};

This tells Babel to use the presets (plugins) we previously installed. Later, when we call babel-loader from Webpack, this is where it will look to know what to do.

In “webpack/client/webpack.common.config”

module.exports = {
entry: {
app: [path.resolve(__dirname, '../../src/index.jsx')],
},
.....
}

module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.(sa|sc|c)ss$/,
use: [
{
loader: ExtractCssChunks.loader,
options: {
hmr: true,
esModule: true,
},
},
{
loader: 'css-loader',
options: {
sourceMap: env === 'development',
modules: {
mode: 'local',
exportGlobals: true,
localIdentName: env === 'development' ? '[name]__[local]__[hash:base64:5]' : '[hash:base64:5]',
context: path.resolve(__dirname, '../../src'),
hashPrefix: 'React Enterprice kit',
},
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: env === 'development',
},
},
{
loader: 'sass-loader',
options: {
sourceMap: env === 'development',
},
},
],
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/i,
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'assets/images',
},
},
],
},

In the configuration, we have rules that tell webpack how to handle a particular asset. The configuration has the following fields:

  1. test: It takes a regular expression that matches the project asset extinction.
  2. loader: Name of loader which will interpret the asset.
  3. use: It takes an array if we want to configure assets with multiple loaders.
  4. options: Optional field which takes output path, name, etc.

output: {
filename: env === 'development' ? '[name].js' : '[name].[hash].js',
path: path.resolve(__dirname, '../../dist'),
chunkFilename: 'scripts/[name].[hash].js',
},

As the options name suggests the

Now we have configured our webpack to at least make a build and give the minifies and optimized code.

Final webpack.common.config.js

module.exports = {
entry: {
app: [path.resolve(__dirname, '../../src/index.jsx')],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.(sa|sc|c)ss$/,
use: [
{
loader: ExtractCssChunks.loader,
options: {
hmr: true,
esModule: true,
},
},
{
loader: 'css-loader',
options: {
sourceMap: env === 'development',
modules: {
mode: 'local',
exportGlobals: true,
localIdentName: env === 'development' ? '[name]__[local]__[hash:base64:5]' : '[hash:base64:5]',
context: path.resolve(__dirname, '../../src'),
hashPrefix: 'React Enterprice kit',
},
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: env === 'development',
},
},
{
loader: 'sass-loader',
options: {
sourceMap: env === 'development',
},
},
],
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/i,
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'assets/images',
},
},
],
},
resolve: {
extensions: ['.js', '.jsx'],
},
mode: process.env.NODE_ENV,
plugins,
optimization: {
splitChunks: {
name: 'vendor',
chunks: 'all',
minChunks: 1,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
},
},
},
moduleIds: 'hashed',
runtimeChunk: 'single',
},
output: {
filename: env === 'development' ? '[name].js' : '[name].[hash].js',
path: path.resolve(__dirname, '../../dist'),
chunkFilename: 'scripts/[name].[hash].js',
},
};

Now in “webpack.dev.config.js” will import the common config and add some more config that is required in development only (i.e live sever to run project etc.)

To run our react app locally we will use the “webpack-dev-server” plugin.

webpack.dev.config.js

const { merge } = require('webpack-merge');
const path = require('path');
const Webpack = require('webpack');
const common = require('./webpack.common.config');

const plugins = [
new Webpack.DefinePlugin({
__CLIENT__: true,
__SERVER__: false,
__DEVELOPMENT__: true,
__DEVTOOLS__: true,
}),
];
if (process.env.analyze) {
plugins.push(
new BundleAnalyzerPlugin({
openAnalyzer: true,
reportFilename: 'bundleReport.html',
analyzerMode: 'static',
token: 'c3e980d3ec23ddd626fecc110501e76a9a469461',
}),
);
}

module.exports = merge(common, {
devtool: 'inline-source-map',
devServer: {
contentBase: path.join(__dirname, '../../dist'),
port: 3000,
hot: true,
},
plugins
});

Here we import the common configurations from “./webpack.common.config” and with “webpack-merge” plugin will merge the dev congif to it. In dev config, we are setting dev to serve by passing the following options:

For analyzing the bundle, we use the “webpack-bundle-analyzer” plugin, which will emit an HTML file to visualize the app’s bundles.

“Webpack.DefinePlugin” plugin set’s the env variable for the mode in which the webpack has been triggered.

Similarly for production, we will configure webpack

webpack.prod.config.js

const path = require('path');
const { merge } = require('webpack-merge');
const Webpack = require('webpack');
const CompressionPlugin = require('compression-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const common = require('./webpack.common.config');

const plugins = [

new Webpack.DefinePlugin({
__CLIENT__: true,
__SERVER__: false,
__DEVELOPMENT__: false,
__DEVTOOLS__: false, // For disabling redux devtools on Production mode
}),
new CompressionPlugin({
algorithm: 'gzip',
filename: '[path].gz[query]',
test: /\.(js|jsx)$|\.css$|\.html$/,
}),
new OptimizeCssAssetsPlugin({
cssProcessorPluginOptions: {
preset: ['default', { discardComments: { removeAll: true } }],
},
}),
new InjectManifest({
swSrc: path.resolve(__dirname, '../../src/service-worker.js'),
maximumFileSizeToCacheInBytes: 5000000,
}),
];

module.exports = merge(common, {
plugins,
});

In production, we need to optimize our assets and also compress them to make the performance faster. So for compressing our assets we are using compression-webpack-plugin which will compress .js, .jsx, .css, .html, etc. to any compression algorithm so it can be served much faster to the server.

And for optimizing/minimizing CSS assets we have used the optimize-css-assets-webpack-plugin which will optimize CSS files in our project by removing whitespace, removing comments, etc.

Instead of writing the whole service-worker by ourselves, we will use “workbox-webpack-plugin/InjectManifest”. The InjectManifest plugin will generate a list of URLs to precache and add that precache manifest to an existing service worker file. It will otherwise leave the file as-is. To learn more visit here

Now we have configured Webpack to run in both development and production mode, so now will setup commands to trigger webpack in package.json

Here will use the better-npm-run package to avoid hearing coded commands in scripts. This will help us in the future as we’ll have many commands to run in scripts.

package.js

"scripts": {
"start-dev": "better-npm-run start-dev",
"build-dev": "better-npm-run build-dev",
"build-prod": "better-npm-run build-prod",
"build-analyze": "better-npm-run build-dev-analyze",
},

"betterScripts": {
"build-dev": {
"command": "webpack --progress --config ./webpack/client/webpack.dev.config.js",
"env": {
"NODE_ENV": "development",
"PORT": 3000
}
},
"build-prod": {
"command": "webpack --progress --config ./webpack/client/webpack.prod.config.js",
"env": {
"NODE_ENV": "production",
"PORT": 3000
}
},
"build-dev-analyze": {
"command": "webpack -w --progress --config ./webpack/client/webpack.dev.config.js",
"env": {
"NODE_ENV": "development",
"PORT": 3000,
"analyze": true
}
},
"start-dev": {
"command": "webpack-dev-server --config ./webpack/client/webpack.dev.config.js --open",
"env": {
"NODE_ENV": "development",
"PORT": 3000
}
}
},

We have added 4 new commands:

  1. “npm run start-dev”: This will start the dev server locally at the given port number in development mode.
  2. “npm run build-dev”: This will trigger a build for “development” mode, And will emit a “dist” in the root.
  3. “npm run build-prod”: This will trigger a build for ” production” mode, And will emit a “dist” in the root.
  4. “npm run build-analyze”: This will trigger a build for development mode with one “bundleReport.html” file which will have all the bundle details.

Now, we will set up out our react application.

In the “./public” folder we need our base “index.html” file and also icons and other necessary files.

“./public/index.html”




React Enterprise Starter Kit









If you're seeing this message, that means
Javascript has been disabled on your browser
Please enable JS to make this app work.



React app will render inside div with id “root”. And here we can also configure our meta tags.

Now for making react app installable for PWA we will also have a “manifest.json” file

“./public/manifest.json”

{
"icons": [
{
"src": "react.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "react.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "react.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "react.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "react.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "react.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"name": "React App",
"short_name": "Application",
"orientation": "portrait",
"display": "standalone",
"start_url": "/",
"description": "Description!",
"background_color": "#01579b",
"theme_color": "#01579b",
"prefer_related_applications": true
}

Now, will set up a basic React application in the “src” folder.

“./src/index.js”

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';

ReactDOM.render(
,
document.getElementById('root'),
);

“app.js”

import React from 'react';

function App() {
return (




React App




);
}

export default App;

Now we are good to start our React app, Run the command “npm run start-dev”

I will cover SSR and more other stuff in the next blog. 🙂

For reference here is the project to which I have been contributing since the beginning React Starter Kit

 

Exit mobile version