Webpack’s Hot Module Replacement (HMR) can update your code without a full page reload, preserving your application’s state.

Let’s see it in action. Imagine this simple React component:

// src/App.js
import React, { useState } from 'react';

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default App;

And a basic webpack.config.js:

// webpack.config.js
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, // Enable HMR
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
    }),
  ],
};

And src/index.js:

// 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(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

When you run npx webpack serve, you’ll see the initial page load. Now, change src/App.js to:

// src/App.js
import React, { useState } from 'react';

function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Current Count: {count}</h1> {/* Changed text */}
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default App;

Save the file. Instead of the entire page refreshing, you’ll notice only the "Count: {count}" text updates to "Current Count: {count}", and crucially, the count state remains 0. This is HMR working.

HMR solves the problem of losing application state (like form inputs, scroll positions, or component states) during development. Traditionally, any code change would trigger a full page reload, resetting everything. HMR allows only the changed module to be re-evaluated and its dependencies updated, while the rest of the running application stays intact.

Internally, webpack-dev-server (when hot: true) sets up a WebSocket connection to your browser. When Webpack detects a change, it compiles the updated module and sends a message over the WebSocket. The webpack-dev-server client-side script, injected into your page, receives this message. It then uses the module.hot API to ask Webpack to load the new module and perform a "hot swap." For React, this often involves react-dom/client.hot.update or similar mechanisms that tell React to re-render only the affected components.

The core levers you control are within webpack.config.js:

  • devServer.hot: true: This is the master switch to enable HMR.

  • plugins: While not strictly HMR configuration, plugins like HtmlWebpackPlugin are essential for setting up the initial HTML structure that webpack-dev-server serves.

  • Module-specific HMR integration: For complex frameworks like React, you often need additional setup. For React, this typically involves wrapping your application’s root render call in a way that HMR can intercept. For example, in src/index.js:

    // src/index.js
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import App from './App';
    
    function renderApp() {
      const root = ReactDOM.createRoot(document.getElementById('root'));
      root.render(
        <React.StrictMode>
          <App />
        </React.StrictMode>
      );
    }
    
    renderApp();
    
    if (module.hot) {
      module.hot.accept('./App', () => {
        // When App.js updates, re-render the app
        renderApp();
      });
    }
    

    This module.hot.accept block tells Webpack: "If App.js changes, don’t just reload the page; instead, re-run the renderApp function." This re-run will pick up the new App component and ReactDOM will efficiently update only the changed parts of the DOM, preserving state within App.

Many developers assume HMR works magically for every framework out of the box. The reality is that while webpack-dev-server provides the infrastructure, each framework or library needs its own HMR integration to tell Webpack how to update its specific components or modules without a full reload. This integration often involves accepting updates for specific modules and re-executing the logic that renders those modules, as seen with the module.hot.accept example. Without this explicit acceptance and re-rendering logic, HMR might fall back to a full page reload even when enabled.

The next hurdle is often understanding how to manage HMR for more complex state management solutions like Redux or Zustand, where the "module" that needs updating isn’t just a single component but a store or a set of reducers.

Want structured learning?

Take the full Webpack course →