Webpack Dev Server’s hot reloading is actually a client-side JavaScript trick, not a magic server-side refresh.

Imagine you’re editing a React component. You save the file. Instead of the whole page reloading, just that one component re-renders with your changes. That’s hot reloading. Here’s how it works:

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development', // Essential for HMR
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  devServer: {
    static: './dist',
    hot: true, // Enable Hot Module Replacement
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
};
// src/index.js
import App from './App'; // Assume App is a React component
import './styles.css';

const rootElement = document.getElementById('root');
let currentApp = App;
let currentInstance = new currentApp();

// Function to render the app
function renderApp() {
  rootElement.innerHTML = ''; // Clear previous content
  currentInstance = new currentApp(); // Create a new instance
  rootElement.appendChild(currentInstance.render());
}

// Initial render
renderApp();

// Hot Module Replacement API
if (module.hot) {
  module.hot.accept('./App.js', async () => {
    // When App.js changes, re-render the component
    currentApp = await import('./App.js').then(module => module.default);
    renderApp(); // Re-render with the new component
  });
}

When you run npx webpack serve, Webpack bundles your code and starts a development server. It also injects a small piece of JavaScript into your application. This "client" script establishes a WebSocket connection back to the Webpack Dev Server. When you save a file, Webpack recompiles only the changed module. It then sends this updated module code over the WebSocket to the client script. The client script receives the new module, checks if it’s part of the hot-swappable code (like a React component), and if so, it tells Webpack’s runtime in your browser to swap out the old module for the new one without a full page reload.

The core problem this solves is developer productivity. Constantly refreshing the entire page after every minor code change, especially in complex SPAs, adds up to significant time wasted. Hot Module Replacement (HMR) allows developers to see the immediate impact of their changes on specific UI elements or logic without losing application state. Think of it as a surgical strike for code updates, not a carpet bomb.

To see this in action:

  1. Create a simple React app:
    npx create-react-app my-hmr-app
    cd my-hmr-app
    
  2. Install html-webpack-plugin and webpack-cli:
    npm install --save-dev html-webpack-plugin webpack-cli
    
  3. Configure webpack.config.js (create if it doesn’t exist):
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      mode: 'development',
      entry: './src/index.js',
      output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist'),
      },
      devServer: {
        static: './dist',
        hot: true,
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: './public/index.html', // Assuming your template is here
        }),
      ],
    };
    
  4. Update src/index.js:
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(<App />);
    
    if (module.hot) {
      module.hot.accept('./App', () => {
        // When App.js changes, re-render the App component
        // The ReactDOM.createRoot(...).render() call will handle the update
        root.render(<App />);
      });
    }
    
  5. Update src/App.js (example):
    import React, { useState } from 'react';
    
    function App() {
      const [count, setCount] = useState(0);
      return (
        <div>
          <h1>Hello HMR!</h1>
          <p>Count: {count}</p>
          <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
      );
    }
    
    export default App;
    
  6. Run the dev server:
    npx webpack serve --config webpack.config.js
    
    Navigate to http://localhost:8080. Click the "Increment" button. Now, edit src/App.js to change the <h1> text. Save the file. You’ll see the <h1> update instantly without the count resetting.

The module.hot.accept() call is the key. It registers a callback that Webpack executes only when the specified module ('./App') changes. Inside this callback, you tell Webpack’s runtime how to update the application. For frameworks like React, often re-rendering the component with the new code is sufficient. Webpack’s HMR runtime handles the actual swapping of the module in memory.

Most people assume HMR magically reloads everything. The truth is, it relies on the application’s own code to properly handle updates. If your module.hot.accept callback doesn’t re-render or re-initialize the affected parts of your app, the changes won’t appear, even though Webpack sent the updated module.

The next hurdle is understanding how to manage state during HMR, especially for more complex scenarios beyond simple component re-renders.

Want structured learning?

Take the full Webpack course →