This article focuses on bootstrapping a react template using Webpack. CLI Tools such as create-react-app, next, Vue CLI, etc provide phenomenal configuration out of the box through which one can generate a project with sane defaults. That being said, seeing how things work under the hood is helpful in light of the fact that sometimes you'll have to make some changes to the defaults. In this article, we'll try to comprehend what webpack is, the way it is valuable and the different things that we can manage with webpack. Alongside this, we'll assemble our own personal React template totally from scratch with a fancy feature of code splitting!
Webpack was announced roughly 6 years ago with it releasing its 5th version - v5 in October of 2020. Webpack 1,2,3 kind of built on the backs of one another whereas Webpack 4 was a complete rewrite from the ground up. So a lot of the plugins that worked with the earlier version, none of them really worked when you moved to v4. Saying this I should say that Webpack 4 and 5 are super quick and performant and can do plenty of cool things with all the new plugins and features. It has gotten over 40k stars on Github. So, what exactly is Webpack? Webpack is an open-source Javascript module bundler.
A module bundler basically builds your dependency tree. So if you were just building a static website you were probably really familiar with either a link tag at the top or a script tag at the bottom. So if you're building a really complex application or a really complex site you're probably used to at this point having to kind of order those link tags or script tags in a specific order. You want the most important thing to be loaded faster and the thing that is dependant upon everything else to be loaded may be more towards the bottom. The job of a module bundler is to build a graph/tree of all the dependencies. Its job is to go through your files and sniff the stuff out to understand what is dependent upon what and once it understands that, it builds this tree/graph and it understands the relationship between one file and the other. It handles all the import statements at the top, the export statements at the bottom, those statements is how the module bundler builds the tree. The next thing it does is create static assets or in other senses, it flattens the files. Sometimes it will minify them or uglify them or concatenate them into one file. In this article, we are going to talk about how not to concatenate them into one big file and maybe split them into various smaller chunks. Since it creates static assets, we can use things like es6, typescript, sass or all of these languages that are built upon the technologies that we know but the browser isn't there yet. The special thing about this bundler is that it is specifically for javascript but we can use plugins for HTML/CSS. The beauty of Webpack is that it is a Javascript module bundler that you can use for any javascript application like React, AngularJs, Angular, Vue or even Node!
The process of splitting your code into smaller chunks so that you only load what you need when you need it. Webpack also has this process called Tree shaking. So it might happen sometimes that you have imported something at the top of your file and haven't used it. If you are using a linter then it might catch it but if for some reason the linter doesn't catch it then webpack will analyse it and remove it off when bundling the code. So tree shaking comes out of the box with webpack but code-splitting needs some work. There are 2 styles or approaches for code splitting.
As I said earlier, it's always good to know how things work under the hood because someday you might have to make changes to the default settings. Adding on to this, you can use webpack any of your javascript projects, no matter the framework or library. Node, React, Angular, Vue, Typescript, etc, you can configure your project with webpack. In simpler terms, the following line may explain why use webpack?
webpack = bower + grunt + gulp + browserify + browser-sync
Webpack is a one-stop solution for all the different libraries that you have used in the past. Also if you see the trends for webpack vs gulp vs grunt, webpack beats them all by a huge margin. Check out the graph below.
![NPM Trends: Webpack v/s Gulp v/s Grunt][./repoimages/npmtrends.png]
https://www.npmtrends.com/gulp-vs-next-vs-webpack-vs-grunt
With such huge number of people using webpack you also get to have have a huge community to help you around.
The current version of React uses ES6 to ES8 syntax. We need Babel to compile the code written in those syntaxes back to code the browser can understand. Babel is there to ensure backward compatibility, awesome right? We can write our code in the newer cleaner syntaxes and have Babel worry about the rest. First we have to install a couple of dependencies and create a babel config file
touch babel.config.jsonyarn add -D @babel/core @babel/polyfill @babel/preset-env @babel/preset-react babel-core babel-loader babel-cli babel-polyfill babel-preset-react
// babel.config.json{"presets": ["@babel/preset-env", "@babel/preset-react"]}
I have set up a very basic react application with some routes using the react-router-dom library. I have also used React Loadable to achieve the Router level code splitting. Following is the react code (Router setup, the rest is plain good old react.)
import React from 'react';// Librariesimport {Router, Route, Switch, Redirect} from 'react-router-dom';import Loadable from 'react-loadable';// Componentsimport ActivityIndicator from '../components/shared/ActivityIndicator';// Helpersimport createBrowserHistory from '../utils/history';// Asynchronous Loading of Pages in different chunksconst AsyncHome = Loadable({loader: () => import('./Home'),loading: ActivityIndicator,});const AsyncAbout = Loadable({loader: () => import('./Home'),loading: ActivityIndicator,});function App() {return (<Router history={createBrowserHistory}><Switch><Route path="/" exact component={AsyncHome} /><Route path="/about" exact component={AsyncAbout} /><Redirect to="/" /></Switch></Router>);}export default App;
People tend to have multiple webpack files, one for development, one for production, one for testing and many more. How I like to do it is, maintain just one file and export a function from it that handles all the cases. As you can see from below, I'm passing an environment variable from my scripts which determines which mode I am in and configures webpack accordingly. Nice and simple, isn't it?
{"scripts": {"build:dev": "webpack --env development","build:prod": "webpack -p --env production","develop": "webpack-dev-server --env development","start": "node server/server.js"}}
You will be using a package called webpack-dev-server which is basically a client-side server with the ability to live reload solely for development purposes. So in your webpack file, you'll be exporting a function that has a parameter called env which is populated through the script. Using this parameter you'll be determining the environment which is then stored in variables called isDev and isProd.
// webpack.config.js filemodule.exports = (env) => {console.log('WEBPACK ENV: ", env);const isProd = env === 'production';const isDev = env === 'development';}
The next few steps are configuring the plugins and setting up other things that we'll be using. Our setup will be divided into 3 major parts:
yarn add dotenv
let envVars;envVars = dotenv.config().parsed || {};envVars.NODE_ENV = env;
// reduce it to a nice object, the same as beforeconst envKeys = Object.keys(envVars).reduce((prev, next) => {prev[`process.env.${next}`] = JSON.stringify(envVars[next]);return prev;}, {});
// Maps environment variables from .env file to the projectconst DefinePlugin = new webpack.DefinePlugin(envKeys);
yarn add -D clean-webpack-plugin
const {CleanWebpackPlugin} = require('clean-webpack-plugin');// Cleans 'dist' folder everytime before a new buildconst CleanPlugin = new CleanWebpackPlugin({root: __dirname,verbose: true,dry: false,});
yarn add -D webpack-bundle-analyzer
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');// Plugin to generate a bundle map with sizesconst AnalyzerPlugin = new BundleAnalyzerPlugin({analyzerMode: 'static', // Set to 'disabled' if you dont want to visualize the output});
yarn add -D html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');const HTMLPlugin = new HtmlWebpackPlugin({template: 'template.html',chunksSortMode: 'none',favicon: './src/assets/static/favicon.ico',});
yarn add -D copy-webpack-plugin
const CopyWebpackPlugin = require('copy-webpack-plugin');// Plugin to copy assets/static directory to the buildconst CopyPlugin = new CopyWebpackPlugin({patterns: [{from: './src/assets/static', to: '.'}],});
Alright! We have all our plugins configured! It's now time to build our webpack configuration! In this part, we'll be creating a config object that is returned by our configuration function which configures webpack. Let's start with creating an empty object which we'll keep on populating.
const config = {};
config.entry = ['babel-polyfill', './src/index.js'];
config.output = {path: path.join(__dirname, '/dist'),filename: 'bundle.js',};
config.optimization = {splitChunks: {cacheGroups: {commons: {test: /[\\]node_modules[\\]/,name: 'vendor',chunks: 'initial',},},},runtimeChunk: {name: 'manifest',},minimizer: [new UglifyJsPlugin({sourceMap: false,uglifyOptions: {ecma: 8,mangle: false,keep_classnames: true,keep_fnames: true,},}),],};
config.plugins = [CleanPlugin,AnalyzerPlugin,HTMLPlugin,CopyPlugin,DefinePlugin,];
yarn add -D babel-loader file-loader html-loader style-loader css-loader
config.module = {rules: [{test: /\.(js|jsx)$/,loader: 'babel-loader',exclude: /node_modules/,},{test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|cot)$/,loader: 'file-loader',},{test: /\.html$/,loader: 'html-loader',},{test: /\.(css|scss)$/i,use: ['style-loader', 'css-loader'],},],};
config.resolve = {extensions: ['.js', '.jsx'],};
if (isProduction) {config.output = {chunkFilename: '[name].[chunkhash].bundle.js',filename: '[name].[chunkhash].bundle.js',path: path.join(__dirname, 'dist'),};config.mode = 'production';}
if (isDev) {config.output = {path: path.join(__dirname, 'dist'),chunkFilename: '[name].bundle.js',filename: '[name].bundle.js',};config.mode = 'development';config.devtool = 'inline-source-map';config.devServer = {contentBase: path.join(__dirname, 'dist'),historyApiFallback: true,open: true,};}
So that's it! You have set up your React project with webpack successfully! We have covered the basics of webpack, its config, plugins and code-splitting. I have set up a complete React Template with other features such as linting and code formatting. You can check out the template here. Feel free to use and customise it!