I'm trying to serve a react app bundled with webpack using a express server. However, hot module reloading is not working when I change my client side (React) code.
Initially, I was using webpack-dev-server to serve the app in which HMR was working perfectly fine. Then I switched to an express server with webpack-dev-middleware and webpack-hot-middleware following this example but now HMR doesn't work anymore.
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const json5 = require("json5");
const webpack = require('webpack');
module.exports = {
mode: process.env.NODE_ENV || 'development',
entry: [
'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000',
'./src/client/index.js'
],
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
publicPath: "/",
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "views", "index.html"),
favicon: path.join(__dirname, "views", "favicon.ico"),
}),
new MiniCssExtractPlugin(),
new CopyPlugin({
patterns: [{
from: path.join(__dirname, 'public'),
to: path.join(__dirname, 'dist'),
}]
}),
new webpack.HotModuleReplacementPlugin(),
],
devServer: {
static: {
directory: path.join(__dirname, "dist"),
},
port: 3000,
historyApiFallback: true,
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ["babel-loader"],
},
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(png|jpg|gif|ico|svg)$/i,
type: 'asset/resource',
},
{
test: /\.json5$/,
type: "json",
parser: {
parse: json5.parse,
},
},
],
},
};
src/server/index.js
const express = require('express');
const cors = require('cors');
const path = require('path');
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static('dist'));
if (process.env.NODE_ENV === 'development') {
(function () {
// Step 1: Create & configure a webpack compiler
const webpackConfig = require(path.resolve(process.cwd(), './webpack.config'));
const compiler = require('webpack')(webpackConfig);
// Step 2: Attach the dev middleware to the compiler & the server
app.use(
require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
})
);
// Step 3: Attach the hot middleware to the compiler & the server
app.use(
require('webpack-hot-middleware')(compiler, {
log: console.log,
path: '/__webpack_hmr',
heartbeat: 10 * 1000,
})
);
})();
}
app.get('*', (req, res) => {
console.log('Serving website!')
res.sendFile(path.resolve('dist', 'index.html'));
});
app.listen(3000, () => {
console.log(`Server is running on port 3000.`);
});
The console shows the following when I try to change any client side code:

I switched to running both the webpack-dev-server and the express server (proxying everything to the webpack-dev-server using the below server code) following this example and this seems to work perfectly fine again.
src/server/index.js
const express = require('express');
const cors = require('cors');
const path = require('path');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackConfig = require(path.resolve(process.cwd(), './webpack.config'));
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static('dist'));
app.get('*', (req, res) => {
console.log('Serving website!')
res.sendFile(path.resolve('dist', 'index.html'));
});
new WebpackDevServer(webpack(webpackConfig), {
devMiddleware: {
publicPath: webpackConfig.output.publicPath,
},
hot: true,
historyApiFallback: true,
proxy: {
"*": "http://localhost:3000"
}
}).listen(8080, 'localhost', (err, result) => {
if (err) {
return console.log(err);
}
console.log('Server is running on port http://localhost:8080/');
});
app.listen(3000, () => {
console.log(`Server is running on port 3000.`);
});
But I don't want to have to run two servers in dev so I wanted to ask if there's a way I can run only the express server and still be able to use HMR in my react app.
P.S. Also attaching my package.json below for reference:
{
"name": "foo-bar",
"version": "0.1.0",
"private": true,
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.1",
"styled-components": "^5.3.8",
"web-vitals": "^2.1.4"
},
"scripts": {
"rs-start": "react-scripts start",
"rs-build": "react-scripts build",
"rs-test": "react-scripts test",
"rs-eject": "react-scripts eject",
"wp-start": "webpack serve --mode development",
"build": "NODE_ENV=development webpack",
"build-prod": "NODE_ENV=production webpack",
"start": "node ./src/server/index.js",
"dev": "NODE_ENV=development nodemon ./src/server/index.js",
"clean": "rm -rf dist",
"burnthemall": "rm -rf node_modules && rm package-lock.json && npm install"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"babel-loader": "^9.1.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"html-webpack-plugin": "^5.5.0",
"json5": "^2.2.3",
"mini-css-extract-plugin": "^2.7.2",
"nodemon": "^2.0.22",
"react-scripts": "^2.1.3",
"webpack": "^5.83.1",
"webpack-cli": "^5.0.1",
"webpack-dev-middleware": "^6.1.1",
"webpack-dev-server": "^4.15.0",
"webpack-hot-middleware": "^2.25.3"
}
}