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 for development (faster builds, better debugging) or production (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 a dist folder in the current directory.
  • module.rules: This is where loaders live.
    • The rule for /\.(js|jsx)$/ tells Webpack to use babel-loader for all .js and .jsx files, excluding node_modules.
    • The options.plugins array within babel-loader is crucial for HMR. react-refresh/babel is a Babel plugin that enables fast refreshes for React components. It’s conditionally included only in development.
    • The rule for /\.css$/ shows how to handle CSS: style-loader injects styles into the DOM, and css-loader interprets @import and url() like import/require().
  • plugins: These perform broader tasks.
    • HtmlWebpackPlugin injects your bundled bundle.js into an index.html file, often copying a template from your public folder.
    • ReactRefreshWebpackPlugin works 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 can import App from './App' instead of import App from './App.jsx'.
  • devServer: Configures the development server. hot: true is 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.

Want structured learning?

Take the full Webpack course →