If you are setting up a React project from scratch, you need to setup a transpiler such as Babel and a bundler such as Webpack. (If you want to know why we need these tools, you can check out this post) Both of these tasks can be done by configuring Webpack and here is a basic webpack config that does it.

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = (env, argv) => {
const devMode = argv.mode !== 'production';

return {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'build'),
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'public', 'index.html'),
}),
new MiniCssExtractPlugin()
],
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: 'defaults' }],
['@babel/preset-react', { runtime: 'automatic' }]
]
}
}
},
{
test: /\.css$/i,
use: [
devMode ? "style-loader" : MiniCssExtractPlugin.loader,
"css-loader"
],
},
{
test: /\.(png|jpe?g|svg)$/i,
type: 'asset/resource'
}
]
}
};
};

This configuration needs to be placed into a file called webpack.config.js. If you don't already have an npm project, you need to create one using npm init -y and place this file within it. Next you need to install all the packages needed by Webpack and React.

Packages needed by React

$ npm install react react-dom

Packages needed by Webpack

$ npm install -D @babel/core @babel/preset-env @babel/preset-react babel-loader css-loader html-webpack-plugin mini-css-extract-plugin style-loader webpack webpack-cli webpack-dev-server

Now you should have everything you need to start running React code. The configuration above expects the base web page to be placed in a file called index.html in the public folder and the initial script to be placed in a file called index.js in the src folder. Here are short examples of both files.

public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

src/index.js

import ReactDOM from 'react-dom/client';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div>I am a React App</div>
);

You can start a dev server for development by running npx webpack serve --mode development in the project folder. You can create a production build for deployment by running npx webpack --mode production. The production build will be placed in a folder named build. To easily run these commands, you can save them as npm scripts as shown in this post.

You can create React components and use them within the index.js file by importing them. These components can import other React components and CSS files.

src/Title/Title.jsx

import './Title.css'

const Title = () => (
<h1>I am a title</h1>
)

src/Title/Title.css

h1 {
color: red;
}

src/index.js

import ReactDOM from 'react-dom/client';
import Title from './Title/Title.jsx';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Title />
);

Similarly, you can import image files into the React component. Webpack will copy these files to the production build and provide a relative URL to the component that imported the image.

import reactIcon from './react.svg';

const Icon = () => (
<img src={reactIcon} alt="React Logo" width={64} height={64} />
);

export default Icon;

Let's walk through the config file and see what each option does.

Sketch of a file icon with the Webpack logo within it

Entry

return {
entry: './src/index.js',
...
}

This tells Webpack the file it should start processing from. It will read this file and process each file that is imported by it. It will then repeat this process for each of the imported files until all the imports are resolved.

Output

return {
...
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'build'),
},
...
}

This tells Webpack where to output the generated assets. In this case, it is set up to output the Javascript to a file called main.js and to output all these assets to a folder called build.

Plugins

HtmlWebpackPlugin

new HtmlWebpackPlugin({
template: path.join(__dirname, 'public', 'index.html'),
})

This plugin outputs a HTML file with the generated Javascript and CSS files referenced through script and link elements. Since React requires a root element to render into, we provide a template HTML file with a root element specified to the plugin (index.html) which it extends with those elements.

MiniCssExtractPlugin

new MiniCssExtractPlugin()

This plugin outputs all the imported CSS files into a single CSS file.

Module

This section of the config tells Webpack what to do with each of the imported files. We do this by specifying rules that mention which loaders need to be used on each type of file.

Rule for processing Javascript and JSX

[
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: 'defaults' }],
['@babel/preset-react', { runtime: 'automatic' }]
]
}
}
},
...
]

This rules matches imported files with a .js or .jsx extension. It then asks Webpack to load these files using babel-loader which then passes each file through Babel. We pass custom options to babel-loader to pass to Babel and Babel will use these options while processing each file.

Babel presets are plugins that customize Babel's behavior. @babel/preset-env allows Babel to output JS code that can run on a given set of browsers. This allows us to use new Javascript language features in the codebase before they are supported by browsers. In this case, we set @babel/preset-env to transform the JS code into code that can run on the defaults set of browsers (i.e. browsers with more than 0.5% of market share and the last two versions of actively developed browsers)

@babel/preset-react is combination of plugins for transforming React component code into JS. Notably, it consists of @babel/plugin-transform-react-jsx which allows Babel to convert JSX code into JS. By setting @babel/preset-react to use the automatic runtime, we ask @babel/plugin-transform-react-jsx to use the new JSX transform where React does not have to be imported into each component file.

Rule for processing CSS

[   
...,
{
test: /\.css$/i,
use: [
devMode ? "style-loader" : MiniCssExtractPlugin.loader,
"css-loader"
],
}
]

This rule matches imported files with a .css extension. It then asks Webpack to first load these files using css-loader which resolves the @import and url() statements in the CSS files. (Webpack applies loaders from the end to the start)

Next it asks Webpack to use style-loader or MiniCssExtractPlugin's loader depending on the mode parameter that is passed into the webpack command. We use style-loader during development because it is faster than the MiniCssExtractPlugin.

Rule for processing image files

[   
...,
{
test: /\.(png|jpe?g|svg)$/i,
type: 'asset/resource'
}
]

This rule matches imported image files with a .png, .jpeg, .jpg or .svg extension. Webpack 5 has built in support for handling assets files that it should copy to the output folder. So we invoke this feature by telling Webpack to treat these files as asset/resource. (In previous versions of Webpack, a separate loader has to be used e.g. file-loader)

Conclusion

A Webpack configuration file can seem scary at first. So I hope that this post helped you be more confident in managing it. You can now extend this basic configuration in many ways such as adding support for SASS or adding support for environment variables.

Prabashwara Seneviratne (bash)

Written by

Prabashwara Seneviratne (bash)

Lead frontend developer