Webpack, when used with React, isn’t just about bundling your code; it’s orchestrating a transformation pipeline that makes modern JavaScript development possible.
Here’s a React app running with HMR (Hot Module Replacement) enabled. Notice how the counter increments without a full page reload:
// src/App.js
import React, { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Hello Webpack!</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default App;
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: [
isDevelopment && require.resolve('react-refresh/babel'),
].filter(Boolean),
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
isDevelopment && new ReactRefreshWebpackPlugin(),
].filter(Boolean),
resolve: {
extensions: ['.js', '.jsx'],
},
devServer: {
static: './dist',
hot: true,
port: 3000,
},
};
This setup tackles a few core problems. First, browsers don’t understand JSX. Babel, via babel-loader, transforms JSX into regular JavaScript function calls that browsers can execute. Second, modern JavaScript features (like ES6+ syntax) might not be supported by all browsers. Babel handles this transpilation too. Finally, for a smoother developer experience, Hot Module Replacement (HMR) allows code changes to be injected into the running application without a full refresh, preserving application state.
The webpack.config.js is the brain.
mode: Tells Webpack whether to optimize fordevelopment(faster builds, better debugging) orproduction(smaller bundles, faster runtime).entry: The starting point of your application’s dependency graph. Webpack follows imports from here.output: Where Webpack spits out the bundled files.path.resolve(__dirname, 'dist')means adistfolder in the current directory.module.rules: This is where loaders live.- The rule for
/\.(js|jsx)$/tells Webpack to usebabel-loaderfor all.jsand.jsxfiles, excludingnode_modules. - The
options.pluginsarray withinbabel-loaderis crucial for HMR.react-refresh/babelis a Babel plugin that enables fast refreshes for React components. It’s conditionally included only indevelopment. - The rule for
/\.css$/shows how to handle CSS:style-loaderinjects styles into the DOM, andcss-loaderinterprets@importandurl()likeimport/require().
- The rule for
plugins: These perform broader tasks.HtmlWebpackPlugininjects your bundledbundle.jsinto anindex.htmlfile, often copying a template from yourpublicfolder.ReactRefreshWebpackPluginworks alongside the Babel plugin to manage the HMR process for React components.
resolve.extensions: Tells Webpack which file extensions to look for when resolving modules, so you canimport App from './App'instead ofimport App from './App.jsx'.devServer: Configures the development server.hot: trueis the magic switch for HMR.
When you run webpack serve (assuming you’ve added it to your package.json scripts), Webpack starts a development server. It watches your files. When a change is detected, it recompiles only the affected modules. The react-refresh Babel plugin and ReactRefreshWebpackPlugin then tell the browser to swap out the old module code for the new, without losing the current state of your React components. This is why your counter doesn’t reset.
The most surprising thing is how little actual "bundling" happens for HMR. Webpack doesn’t re-bundle the entire application on every save. Instead, it creates a runtime client that listens for updates. When a module changes, it sends a signal to this client, which then requests the updated module from the server and applies it directly to the running code in the browser using JavaScript’s dynamic import capabilities and the module system.
You’ll next want to explore code splitting with Webpack to optimize your production builds.