If you are coming from react-hot-loader
, before using the plugin,
you have to ensure that you've completely erased the integration of RHL from your app:
- Removed the
react-hot-loader/babel
Babel plugin (from Babel config variants or Webpack config) - Removed the
react-hot-loader/patch
Webpack entry - Removed the
react-hot-loader/webpack
Webpack loader - Removed the alias of
react-dom
to@hot-loader/react-dom
(frompackage.json
or Webpack config) - Removed imports and their usages from
react-hot-loader
This has to be done because, internally,
react-hot-loader
intercepts and reconciles the React tree before React can try to re-render it.
That in turn breaks mechanisms the plugin depends on to deliver the experience.
Our socket implementation depends on the DOM URL API, and as a consequence, a polyfill is needed when running in IE11.
The plugin by default will detect whether the URL
and URLSearchParams
constructors are available on the global scope,
and will fallback to a pony-fill approach (polyfill without global scope pollution) when it is not.
If for some reason you need to force this behaviour,
e.g. working on browsers with a broken URL implementation,
you can use the overlay.useURLPolyfill
option:
module.exports = {
plugins: [
new ReactRefreshPlugin({
overlay: {
useURLPolyfill: true,
},
}),
],
};
npm@7
have brought back the behaviour of auto-installing peer dependencies with new semantics,
but their support for optional peer dependencies,
used by this plugin to provide support to multiple integrations without bundling them all,
are known to be a bit lacking.
If you encounter the ERESOLVE
error code while running npm install
-
you can fallback to use the legacy dependency resolution algorithm and it should resolve the issue:
npm install --legacy-peer-deps
This plugin does not provide HMR for CSS.
To achieve that,
you should be using style-loader
or mini-css-extract-plugin
.
Both provides HMR capabilities out of the box for Webpack 5 -
if are still on Webpack 4 and uses mini-css-extract-plugin
, you might have to do some setup.
If you share the Babel config for files in an indirect code path (e.g. Web Workers, JS Templates with partial pre-render) and all your other source files, you might experience this error:
Uncaught ReferenceError: $RefreshReg$ is not defined
The reason is that when using child compilers (e.g. html-webpack-plugin
, worker-plugin
), plugins are usually not applied (but loaders are).
This means that code processed by react-refresh/babel
is not further transformed by this plugin and will lead to broken builds.
To solve this issue, you can choose one of the following workarounds:
Sloppy
In the entry of your indirect code path (e.g. some index.js
), add the following two lines:
self.$RefreshReg$ = () => {};
self.$RefreshSig$ = () => () => {};
This basically acts as a "polyfill" for helpers expected by react-refresh/babel
, so the worker can run properly.
Simple
Ensure all exports within the indirect code path are not named in PascalCase
.
This will tell the Babel plugin to do nothing when it hits those files.
In general, the PascalCase
naming scheme should be reserved for React components only,
and it doesn't really make sense for them to exist within non-React-rendering contexts.
Robust but complex
In your Webpack configuration, alter the Babel setup as follows:
{
rules: [
// JS-related rules only
{
oneOf: [
{
test: /\.[jt]s$/,
include: '<Your indirection files here>',
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
// Your Babel config here
},
},
},
{
test: /\.[jt]sx?$/,
include: '<Your files here>',
exclude: ['<Your indirection files here>', /node_modules/],
use: {
loader: 'babel-loader',
options: {
// Your Babel config here
plugins: [isDevelopment && 'react-refresh/babel'].filter(Boolean),
},
},
},
],
},
// Any other rules, such as CSS
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
];
}
This would ensure that your indirect code path will not be processed by react-refresh/babel
,
thus eliminating the problem completely.
This means that you have not enabled HMR for Webpack, or we are unable to detect a working HMR implementation from the compilation context.
Using webpack-dev-server
Set the hot
or hotOnly
options to true
.
Using webpack-hot-middleware
Add HotModuleReplacementPlugin
to your list of plugins to use in development mode.
Using webpack-plugin-serve
Set the hmr
option to true
.
This is intended behaviour. If you're coming from react-hot-loader, this might be the biggest surprise for you.
The main rationale behind this is listed in this wish list for hot reloading:
It is better to lose local state than to behave incorrectly.
It is better to lose local state than use an old version.
The truth is, historically, hot reloading for class components was never reliable and maintainable. It was implemented based on workarounds like wrapping components with Proxies.
While these workarounds made hot reloading "work" on the surface, they led to inconsistencies in other departments:
- Lifecycle methods will fire randomly at random times;
- Type checks will fail randomly (
<Component />.type === Component
isfalse
); and - Mutation of React's internals, which is difficult to manage and will need to play catch up with React as we move into the future (a-la Concurrent Mode).
Thus, to keep fast refresh reliable and resilient to errors (with recovery for most cases), class components will always be re-mounted on a hot update.
In most cases, if the plugin is applied correctly, it would mean that we weren't able to set up boundaries to stop update propagation. It can be narrowed down to a few unsupported patterns:
-
Un-named/non-pascal-case-named components
See this tweet for drawbacks of not giving component proper names. They are impossible to support because we have no ways to statically determine they are React-related. This issue also exist for other React developer tools, like the hooks ESLint plugin. Internal components in HOCs also have to conform to this rule.
// won't work export default () => <div />; export default function () { return <div />; } export default function divContainer() { return <div />; }
-
Chain of files leading to root with none containing React-related content only
This pattern cannot be supported because we cannot ensure non-React-related content are free of side effects. Usually with this error you will see something like this in the browser console:
Ignored an update to unaccepted module ... [a very long path]
-
export * from 'namespace'
(TypeScript only)This only affect users using TypeScript on Babel. This pattern is only supported when you don't mix normal exports with type exports, or when all your exports conform to the
PascalCase
rule. This is because we cannot statically analyse the exports from the namespace to determine whether we can set up a boundary and stop update propagation.
Webpack allows various bundle splitting techniques to improve performance and cacheability. However, these techniques often result in a shuffled execution order, which will break fast refresh.
To make fast refresh work properly, make sure your Webpack configuration comply to the following rules:
-
All React-related packages (including custom reconcilers) should be in the same chunk with
react-refresh/runtime
Because fast refresh internally uses the React DevTools protocol and have to be registered before any React code runs, all React-related stuff needs to be in the same chunk to ensure execution order and object equality in the form of
WeakMap
keys.Using DLL plugin
Ensure the entries for the DLL include
react-refresh/runtime
, and themode
option is set todevelopment
.module.exports = { mode: 'development', entry: ['react', 'react-dom', 'react-refresh/runtime'], };
Using multiple entries
Ensure the
react
chunk includesreact-refresh/runtime
.module.exports = { entry: { main: 'index.js', vendors: ['react', 'react-dom', 'react-refresh/runtime'], }, };
-
Only one copy of both the HMR runtime and the plugin's runtime should be embedded for one Webpack app
This concern only applies when you have multiple entry points. You can use Webpack's
optimization.runtimeChunk
option to enforce this.module.exports = { optimization: { runtimeChunk: 'single', }, };
Fast refresh relies on initialising code before ANY React code is ran. If you externalise React, however, it is likely that this plugin cannot inject the necessary runtime code before it.
You can deal with this in a few ways (also see #334 for relevant discussion).
Production-only externalisation
The simplest solution to this issue is to simply not externalise React in development. This would guarantee any code injected by this plugin run before any React code, and would require the least manual tweaking.
Use React DevTools
If the execution environment is something you can control, and you wanted to externalise React in development, you can use React DevTools which would inject hooks to the environment for React to attach to.
React Refresh should be able to hook into copies of React connected this way even it runs afterwards,
but do note that React DevTools does not inject hooks over a frame boundary (iframe
).
Externalise React Refresh
If all solutions above are not applicable, you can also externalise react-refresh/runtime
together with React.
Using this, however, would require you to ensure the injected entry from this plugin is executed before React. You can check out this sandbox for an example on how this could be done.
If you are running on a micro-frontend architecture (e.g. Module Federation in Webpack 5),
you should set the library
output to ensure proper namespacing in the runtime injection script.
Using Webpack's output.uniqueName
option (Webpack 5 only)
module.exports = {
output: {
uniqueName: 'YourLibrary',
},
};
Using Webpack's output.library
option
module.exports = {
output: {
library: 'YourLibrary',
},
};
Using the plugin's library
option
module.exports = {
plugins: [
new ReactRefreshPlugin({
library: 'YourLibrary',
}),
],
};
In webpack-dev-server@3
,
there is a bug causing it to mis-judge the runtime environment when the Webpack 5 browserslist
target is used.
It then fallbacks to thinking a non-browser target is being used, in turn skipping injection of the HMR runtime, and thus breaking downstream integrations like this plugin.
To overcome this,
you can conditionally apply the browserslist
only in production modes in your Webpack configuration:
module.exports = {
target: process.env.NODE_ENV !== 'production' ? 'web' : 'browserslist',
};