'Webpack5 + react-refresh-webpack-plugin does not work

I wanted to setup react with webpack, babel, and typescript myself since I would like to know more and have a consistent boilerplate for development.

I have been trying to setup fast refreshing and hot reloading in react with `webpack5 following the documentation for react-refresh-webpack-plugin. I was not getting anywhere and decided to make a minimal example that reproduces the error.

My expectation is that I should be able to make changes to my source folder and the should appear immediately or at least upon refresh. As of now, neither happens.

I made sure to follow the primary readme for react-refresh-webpack-plugin as best I could. I will review it again and see what changes I can make according to it and report back here.

The file tree so far looks like :

├── babel.config.js
├── package-lock.json
├── package.json
├── src
│   ├── app.tsx
│   ├── index.html
│   └── index.tsx
├── tsconfig.json
└── webpack.config.js

my webpack config

// webpack.config.js

const path = require( 'path' )
const ReactRefreshPlugin = require( '@pmmmwh/react-refresh-webpack-plugin' )
const HTMLWebpackPlugin = require( 'html-webpack-plugin' )


// Ingredients
// Just to make things less ugly

const SRC = path.join( __dirname, 'src' )
const NODE_MODULES = /node_modules/
const MODE = 'development'
const INDEX = path.join( SRC, 'index.html' )
const ENTRY_POINT =  path.join( SRC, 'index.tsx' )


// Objects nested within exports.
// Also to make things less ugly.
const PLUGINS = [
    new ReactRefreshPlugin(),
    new HTMLWebpackPlugin({ 
        template : INDEX,
        filname : './index.html'
    })
]
const RESOLVE = { extensions : [
    '.ts',
    '.js',
    '.tsx',
    '.jsx'
]}
const RULES = [
    // Babel
    {
        test : /\.tsx?$/,
        exclude : NODE_MODULES,
        include : SRC,
        use : 'babel-loader'
    }
]


// Prettier exports.
module.exports = ( env ) => {
    const config = {
        mode : MODE,
        entry : ENTRY_POINT,
        module : {
            rules : RULES
        },
        plugins : PLUGINS,
        resolve : RESOLVE
    }
    console.log( JSON.stringify( config, null, space = "\n" ) )
    return config
}

and my babel config

// babel.config.js

// Variables
const PRODUCTION = 'production'  

// Methods 
const PRESETS = ( api ) => [
    '@babel/preset-env',
    '@babel/preset-typescript',
    [
        '@babel/preset-react',
        {
            development : !api.env( PRODUCTION ),
            runtime : 'automatic'
        }
    ]
]

const PLUGINS = ( api ) => [
    api.env( PRODUCTION ) && 'react-refresh/babel'
].filter( Boolean )


module.exports = ( api ) => {

    api.cache.using(
        () => process.env.NODE_ENV
    )

    return {
        presets : PRESETS( api ),
        plugins : PLUGINS( api )
    }


}

package.json:

{
  "name": "typescript-react-with-refresh",
  "version": "1.0.0",
  "description": "Trying to get hot reloading working with webpack5.",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve --hot",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.16.7",
    "@babel/preset-env": "^7.16.7",
    "@babel/preset-react": "^7.16.7",
    "@babel/preset-typescript": "^7.16.7",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
    "@types/react": "^17.0.38",
    "@types/react-dom": "^17.0.11",
    "babel-loader": "^8.2.3",
    "html-webpack-plugin": "^5.5.0",
    "react-refresh": "^0.11.0",
    "typescript": "^4.5.4",
    "webpack": "^5.65.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.7.2"
  },
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}

and finally the configuration for the typescript compiler:

// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "ESNext",
    "moduleResolution": "node",
    "outDir": "dist",
    "target": "ESNext",
    "sourceMap": true
  },
  "include": ["src"]
}

Finally, if index.html or any other source files are required, I can provide those. For now I am not providing them to keep this post brief. Thank you in advance for any help!

Update

I found that I should have installed ts-node and type-fest as dev dependencies, and change webpack.config.js to webpack.config.ts. Nonetheless the problem persists.



Solution 1:[1]

Figured it out. It turns out that there was a similar issue on the react-refresh-webpack-plugin issues page, see issue #334. Part of my solution was to restart since I decided I did not like the formatting. The problem was with externalization of react. The fix in particular was to add the following amendments to the optimization and entry sections below files and merge them together:

const path = require('path');
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');


const SRC = path.join( __dirname, '..', 'src' )
const DIST = path.join(__dirname, '..', 'dist')
const PUBLIC = path.join( __dirname, '..', 'public' )

module.exports = (env) => {

  return {

    mode: 'development',
    entry: {
      reactRefreshSetup: '@pmmmwh/react-refresh-webpack-plugin/client/ReactRefreshEntry.js',
      main: path.join( SRC, 'index.tsx' ),
    },

    output: {
      filename: '[name].js',
      path: DIST,
    },

    module: {
      rules: [
        {
          test: /\.tsx?$/,
          include: SRC,
          use: 'babel-loader',
        },
        {
          test : /\.css$/,
          use : [
            'style-loader',
            'css-loader'
          ]
        },
      ],
    },

    plugins: [
      new HtmlWebpackPlugin({
        filename: './index.html',
        template: path.join( PUBLIC, 'index.html' ),
      }),
    ],

    resolve: {
      extensions: ['.js', '.jsx', '.ts', '.tsx' ],
    },

  }

}

and the rest

module.exports = ( env ) => {

  return {

    devServer: {
      hot: true,
      port: 8080,
    },

    plugins : [
      new ReactRefreshPlugin()
    ],

    optimization: {
      runtimeChunk: 'single',
      // Ensure `react-refresh/runtime` is hoisted and shared
      // Could be replicated via a vendors chunk
      splitChunks: {
        chunks: 'all',
        name(_, __, cacheGroupKey) {
          return cacheGroupKey;
        },
      },
    }

  }

}

Solution 2:[2]

This is an example that works:

devServer: {
  hot: true,
  open: true,
  port: 7777,
  static: {
    directory: path.resolve(pRoot, 'public', 'assets'),
    publicPath: '/public/assets/',
  },
},

The key here is to remove these two fields, since their jobs are handled by the HMR plugin instead:

devServer: {
  watchFiles: ['src', 'public'],
  historyApiFallback: true,
  // ...
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Kindred