Closed shirakaba closed 4 years ago
@drather19 I tried cloning and running your repo but the dev server never starts up.
Environment, OS - OSX 10.14.6 Node - v12.13.0 Yarn -1.19.2
@pmmmwh - FYI
react-app-rewired-react-refresh on master is 📦 v0.1.0 via ⬢ v12.13.0 ❯ yarn start yarn run v1.19.2 $ react-app-rewired start | cat ℹ 「wds」: Project is running at http://192.168.1.178/ ℹ 「wds」: webpack output is served from / ℹ 「wds」: Content not from webpack is served from /Users/seanmatheson/Development/temp/react-app-rewired-react-refresh/public ℹ 「wds」: 404s will fallback to /index.html Starting the development server...
This is fixed in the plugin's master
branch, and will be released tomorrow.
I managed to get @pmmmwh 's webpack plugin working with a TypeScript React app using babel. However, the incremental builds take around 12 seconds instead of ~2 seconds with just ts-loader. I'm going to keep playing with this to see if I'm missing something on the babel config side which makes the performance closer, but for now it's a wash compared to ts-loader and full refreshes.
@IronSean Please report it in the repo of that plugin? This doesn't sound normal.
I'm going to keep playing with this to see if I'm missing something on the babel config side which makes the performance closer, but for now it's a wash compared to ts-loader and full refreshes.
Mind posting your config/setup there? I won't be able to figure out the issues without more context.
@pmmmwh I opened this issue to move the discussion to your repo once I confirmed it was indeed your plugin making the difference: https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/20
Will react-refresh
(React Fast Refresh
?) work with Preact, or is react-hot-loader
the long-term solution for Preact?
@Jumblemuddle that depends on Preact but they should be able to integrate with Fast Refresh if they want to.
For CRA folks wanting to run with Fast Refresh, I have had better luck with craco (vs. react-app-rewired+customize-cra) now via the following craco.config.js
:
// eslint-disable-next-line
const { whenDev } = require('@craco/craco');
// eslint-disable-next-line
const ReactRefreshPlugin = require('react-refresh-webpack-plugin');
module.exports = {
webpack: {
configure: webpackConfig => {
if (process.env.NODE_ENV === 'development') {
webpackConfig.module.rules.push({
test: /BabelDetectComponent\.js/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
plugins: [require.resolve('react-refresh/babel')],
},
},
],
});
webpackConfig.module.rules.push({
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
presets: [
'@babel/react',
'@babel/typescript',
['@babel/env', { modules: false }],
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
'react-refresh/babel',
],
},
},
],
});
}
return webpackConfig;
},
plugins: [
...whenDev(
() => [new ReactRefreshPlugin({ disableRefreshCheck: false })],
[],
),
],
},
};
In particular, adding webpackConfig.optimization.runtimeChunk = false;
will let you add/remove hooks and still gracefully fast refresh.
Enjoying the improved experience even more now. Thanks to @mmhand123 for the tip via https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/25! (<-- resolved!)
Based on the suggestion by @drather19 I have published a customize-cra plugin to make it easier. See esetnik/customize-cra-react-refresh.
Thanks to @drather19 I slightly modify the code now it can work in a yarn workspace monorepo setup.
First, install the following in the sub-packages you want to enable fast refresh:
"@craco/craco": "^5.6.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.2.0", "webpack-hot-middleware": "^2.25.0"
Then add this to craco.config.js
:
;(function ForbidCRAClearConsole() {
try {
require('react-dev-utils/clearConsole')
require.cache[require.resolve('react-dev-utils/clearConsole')].exports = () => {}
} catch (e) {}
})()
const { whenDev } = require('@craco/craco')
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
module.exports = {
webpack: {
configure: webpackConfig => {
whenDev(() => {
// Work around monorepo setup when using yarn workspace hoisted packages
// without the need to list 'babel-loader' and 'babel-preset-react-app' as
// dependencies to avoid duplication since 'react-scripts' already has them.
const reactLoaderConfig = webpackConfig.module.rules
.find(x => Array.isArray(x.oneOf))
.oneOf.find(
x =>
x.options &&
x.options.presets &&
x.options.presets.some(p => p.includes('babel-preset-react-app')) &&
x.loader &&
typeof x.loader.includes === 'function' &&
x.loader.includes('babel-loader') &&
x.test &&
typeof x.test.test === 'function' &&
x.test.test('x.tsx') &&
x.test.test('x.jsx'),
)
if (reactLoaderConfig) {
webpackConfig.module.rules.push({
test: /BabelDetectComponent\.js/,
use: [
{
loader: reactLoaderConfig.loader,
options: {
plugins: [require.resolve('react-refresh/babel')],
},
},
],
})
webpackConfig.module.rules.push({
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: [
{
loader: reactLoaderConfig.loader,
options: {
presets: reactLoaderConfig.options.presets,
plugins: [require.resolve('react-refresh/babel')],
},
},
],
})
} else {
console.error('cannot find react app loader')
}
// console.debug(require('util').inspect(webpackConfig.module.rules, { colors: true, depth: null }))
})
return webpackConfig
},
plugins: [whenDev(() => new ReactRefreshPlugin({ disableRefreshCheck: false }))].filter(Boolean),
},
}
@gaearon Do we expect Fast Refresh to become available in the CRA by default at some point in time? if so what is required for that?
Some amount of work is required for that :-) which is currently being done.
if use HMR functions will be called ? for example componentDidMount. I use react-proxy and componentDidMount will be called. And react 15.X can use Fast Refresh?
componentDidMount
will be called. As well as unmount
- classes would be reloaded in full.react-proxy
. Well, you should stopped a few years ago probably.15.X
? - absolutely not. Fast Refresh is a part of react, and thus exists only in a modern versions.so we should use Fast Refresh or react-hot-loader to replace react-proxy? Is there a way to prevent functions(componentDidMount) from executing for HMR? - it will call method to get new data.
How should i use react-hot-loader in JIT ? - Browser real-time compilation
so we should use Fast Refresh or react-hot-loader to replace react-proxy?
Try
fast refresh
first, thenRHL
Is there a way to prevent functions(componentDidMount) from executing for HMR? - it will call method to get new data.
(use hooks...), do not rely on component lifeCycle, fetch the data when needed. Try
react-query
,swr
or other solutions.
As for the question about how the new HMR should be used, I don't think I know the latest thinking there. I see @gaearon has a wip PR over on the CRA repo:
To clarify for readers, that PR is very outdated and not relevant anymore.
I need to write something down about how Fast Refresh works and how to integrate it. Haven't had time yet.
As of today, that PR is still open. It would be nice if only relevant PRs that still have a chance to be merged would be kept open to have a better overview. If you're just keeping them as a reference, i'd recommend moving stuff to a branch, tag or different repository.
I keep getting Error: [React Refresh] Hot Module Replacement (HMR) is not enabled! React Refresh requires HMR to function properly.
I've followed the documentation but seems like I might've missed something?
I keep getting Error: [React Refresh] Hot Module Replacement (HMR) is not enabled! React Refresh requires HMR to function properly. I've followed the documentation but seems like I might've missed something?
@silkfire I'm assuming you're using the webpack plugin. If yes, please file your question in the webpack plugin repo: https://github.com/pmmmwh/react-refresh-webpack-plugin/.
As of today, that PR is still open. It would be nice if only relevant PRs that still have a chance to be merged would be kept open to have a better overview. If you're just keeping them as a reference, i'd recommend moving stuff to a branch, tag or different repository.
I appreciate your suggestion, but with thousands of unread notifications it can sometimes be difficult for me to remember to revisit old PRs. I trust the Create React App repository maintainers to do the right thing and close if they consider it not useful anymore.
I'm going to close this.
We have https://github.com/pmmmwh/react-refresh-webpack-plugin/ as a reference implementation for webpack. And https://github.com/facebook/react/issues/16604#issuecomment-528663101 explains how to make a custom integration.
I keep getting
Error: [React Refresh] Hot Module Replacement (HMR) is not enabled! React Refresh requires HMR to function properly.
I've followed the documentation but seems like I might've missed something?
It seems like you haven't enabled webpack HMR. For further help please file an issue in the plugin's repo.
As Hot Replacement is now a part of React - should it have a separate place in the React documentation, pointing to the additional libraries to be used with particular bundlers and platforms, as well as explaining some still existing gotchas, like with self-updating css modules.
Information like this should not be buried in github issues and blog posts.
@theKashey it's in React, but the react-dom implementation is only experimental, for one. Also, there's a fast refresh implementation that will be bundled with create-react-app, but it hasn't been released yet: pmmmwh/react-refresh-webpack-plugin#7. Perhaps it will be in the next react-scripts version.
So probably the React team currently do not feel it right to talk about Fast Refresh for react-dom in this experimental phase yet.
it's in React, but the react-dom implementation is only experimental, for one.
To be clear, the implementation in react-dom
itself is stable, just like in React Native. It's just that the integrations are not all stable.
should it have a separate place in the React documentation, pointing to the additional libraries to be used with particular bundlers and platforms, as well as explaining some still existing gotchas, like with self-updating css modules.
This sounds reasonable. I'd be happy to take a PR adding it to the Advanced Guides section, maybe based on similar RN page.
@gaearon
My react app is okay with some styled component changes and correctly applying those changes without any problems.
However, when I change some code in a Redux's reducer, whole app is hard-refreshed and loses all the redux states.
Do I need to use some other libraries like redux-persist
to save the current state along with react-fast-refresh
?
We've went a full circle and here we go again 😅
That's how low level HMR is working, and is outside of fast-refresh responsibility. Please refer to redux or webpack docs
We've went a full circle and here we go again 😅
That's how low level HMR is working, and is outside of fast-refresh responsibility. Please refer to redux or webpack docs
Would you link the full circle reference?
Would you link the full circle reference?
The same questions were asked for React Hot Loader. The same answers were given. We are in the beginning of a new cycle.
@jwchang0206 Look at redux-reducers-injector, a small library I wrote for addressing this issue. It will allow you to support reducers reloading with hot reloading. Make sure you follow the redux principles of immutability in your reducers and it will work smooth 💯 And if you are using sagas, you can use redux-sagas-injector.
@gaearon I'm a bit confused by the use of window
. It doesn't look to me as if it is really necessary because the implementation is swapped out? What's the point of that?
var prevRefreshReg = window.$RefreshReg$; // these are dummies
var prevRefreshSig = window.$RefreshSig$; // these are dummies
var RefreshRuntime = require('react-refresh/runtime');
window.$RefreshReg$ = (type, id) =>{ /*...*/ }
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
try {
// ...
} finally {
window.$RefreshReg$ = prevRefreshReg; // these are dummies again
window.$RefreshSig$ = prevRefreshSig; // these are dummies again
}
I have my own custom bundler and I'm the process of implementing this but I can't see why that would be an absolute must or what the point of it would be... initially I suspected some memory usage/leakage optimization but these are just calls forwarded to the RefreshRuntime
...
@leidegre I can’t comment on the decision to set $RefreshSig$ on the window object, but the coupling to a browser environment gave me problems when consuming Fast Refresh in React NativeScript. @pmmmwh came to the rescue by adapting his Fast Refresh Webpack plugin to overcome Fast Refresh’s coupling to the browser (issues encountered and overcome were discussed in this thread: https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/79). I wonder if the approach used would be of any use to you in your custom bundler’s integration of Fast Refresh.
My bundler is mostly a wrapper around the TypeScript compiler. The implementation is mostly this, adapted from the react-refresh/babel
visitor.
This is just a simple thing that works but it's not as complete as the react-refresh/bable
visitor.
import ts = require("typescript")
import { IndexModule } from "./registry"
/** Enables the use of `react-refresh` for hot reloading of React components. */
export function hotTransform(m: IndexModule, hot: boolean) {
// see https://github.com/facebook/react/issues/16604#issuecomment-528663101
return (ctx: ts.TransformationContext) => {
return (sourceFile: ts.SourceFile) => {
const refreshRuntime = ts.createUniqueName("ReactRefreshRuntime")
const createSignatureFunctionForTransform = ts.createPropertyAccess(
refreshRuntime,
"createSignatureFunctionForTransform"
)
const register = ts.createPropertyAccess(refreshRuntime, "register")
let hasComponents = false
function visitor(node: ts.Node): ts.VisitResult<ts.Node> {
if (ts.isFunctionDeclaration(node)) {
if (_hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
// assert component naming convention
if (node.name === undefined) {
console.warn("unsupported export of unnamed function in ...")
return node
}
const name = node.name
if (!_isComponentName(name.text)) {
console.warn(
`warning: unsupported export '${name.text}' in ${m.path} (${m.id}) does not look like a function component, component names start with a capital letter A-Z. TSX/JSX files should only export React components.`
)
return node
}
if (!hot) {
return node // opt-out
}
hasComponents = true
let hookSignatureString = ""
function hookSignatureStringVisitor(
node: ts.Node
): ts.VisitResult<ts.Node> {
const hookSig = _getHookSignature(node)
if (hookSig !== undefined) {
if (0 < hookSignatureString.length) {
hookSignatureString += "\n"
}
hookSignatureString += hookSig
}
return node
}
// update function body to include the call to create signature on render
const signature = ts.createUniqueName("s")
node = ts.visitEachChild(
node,
(node) => {
if (ts.isBlock(node)) {
return ts.updateBlock(
ts.visitEachChild(node, hookSignatureStringVisitor, ctx),
[
ts.createExpressionStatement(
ts.createCall(signature, undefined, [])
),
...node.statements,
]
)
}
return node
},
ctx
)
const signatureScope = ts.createVariableStatement(
undefined,
ts.createVariableDeclarationList(
[
ts.createVariableDeclaration(
signature,
undefined,
ts.createCall(
createSignatureFunctionForTransform,
undefined,
undefined
)
),
],
ts.NodeFlags.Const
)
)
const createSignature = ts.createExpressionStatement(
ts.createCall(signature, undefined, [
name,
ts.createStringLiteral(hookSignatureString),
])
)
const registerComponent = ts.createExpressionStatement(
ts.createCall(register, undefined, [
name,
ts.createStringLiteral(m.path + " " + name.text),
])
)
return [signatureScope, node, createSignature, registerComponent]
}
}
if (!hot) {
// if hot reloading isn't enable, remove hot reloading API calls
if (ts.isExpressionStatement(node)) {
const call = node.expression
if (ts.isCallExpression(call)) {
if (
_isPropertyAccessPath(
call.expression,
"module",
"hot",
"reload"
)
) {
return undefined
}
}
}
}
return node
}
sourceFile = ts.visitEachChild(sourceFile, visitor, ctx)
if (hot && hasComponents) {
let reactIndex = sourceFile.statements.findIndex((stmt) => {
if (ts.isImportEqualsDeclaration(stmt)) {
const ref = stmt.moduleReference
if (ts.isExternalModuleReference(ref)) {
const lit = ref.expression
if (ts.isStringLiteral(lit)) {
return lit.text === "react"
}
}
}
return false
})
if (reactIndex === -1) {
console.warn(`cannot find import React = require('react') in ...`)
reactIndex = 0
}
// insert after
sourceFile = ts.updateSourceFileNode(sourceFile, [
...sourceFile.statements.slice(0, reactIndex + 1),
ts.createImportEqualsDeclaration(
undefined,
undefined,
refreshRuntime,
ts.createExternalModuleReference(
ts.createStringLiteral("react-refresh/runtime")
)
),
...sourceFile.statements.slice(reactIndex + 1),
ts.createExpressionStatement(
ts.createCall(
ts.createPropertyAccess(
ts.createPropertyAccess(
ts.createIdentifier("module"),
ts.createIdentifier("hot")
),
ts.createIdentifier("reload")
),
undefined,
undefined
)
),
ts.createExpressionStatement(
ts.createBinary(
ts.createPropertyAccess(
ts.createIdentifier("globalThis"),
ts.createIdentifier("__hot_enqueueUpdate")
),
ts.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
ts.createCall(
ts.createPropertyAccess(
ts.createIdentifier("globalThis"),
ts.createIdentifier("__hot_enqueueUpdate")
),
undefined,
undefined
)
)
),
])
}
return sourceFile
}
}
}
function _hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
const modifiers = node.modifiers
if (modifiers !== undefined) {
for (let i = 0; i < modifiers.length; i++) {
if (modifiers[i].kind === kind) {
return true
}
}
}
return false
}
function _isComponentName(name: string): boolean {
// ^[A-Z]
const ch0 = name.charCodeAt(0)
return 0x41 <= ch0 && ch0 <= 0x5a
}
function _isPropertyAccessPath(
node: ts.Expression,
...path: ReadonlyArray<string>
): node is ts.PropertyAccessExpression {
for (let i = 0; i < path.length; i++) {
if (ts.isPropertyAccessExpression(node)) {
if (!(node.name.text === path[path.length - (i + 1)])) {
return false
}
node = node.expression
}
}
return true
}
function _getHookSignature(node: ts.Node): string | undefined {
if (ts.isExpressionStatement(node)) {
const call = node.expression
if (ts.isCallExpression(call)) {
const prop = call.expression
if (ts.isPropertyAccessExpression(prop)) {
const text = prop.name.text
if (text.startsWith("use") && 3 < text.length) {
// todo: add additional checks and emit warnings if the hook usage looks non standard
return text
}
}
}
}
return undefined
}
I wasn't sure how to use createSignatureFunctionForTransform
at first but it's just a factory function that creates a little state machine for each component. So you call it once for each function with the static hook signature (which is just an opaque value, akin to a hash). You then call it from render for it to finish it's setup work.
It changes something like this:
import React = require("react")
export function App() {
const [state, setState] = React.useState(0)
return (
<React.Fragment>
<p>
Click Count !!!<strong>{state}</strong>!!!
<br />
<button onClick={() => setState((acc) => acc + 1)}>Click me</button>
</p>
</React.Fragment>
)
}
Into this:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const React = require("react");
const ReactRefreshRuntime_1 = require(6);
const s_1 = ReactRefreshRuntime_1.createSignatureFunctionForTransform();
function App() {
s_1();
const [state, setState] = React.useState(0);
return (React.createElement(React.Fragment, null,
React.createElement("p", null,
"Click Count !!!",
React.createElement("strong", null, state),
"!!!",
React.createElement("br", null),
React.createElement("button", { onClick: () => setState((acc) => acc + 1) }, "Click me"))));
}
exports.App = App;
s_1(App, "useState");
ReactRefreshRuntime_1.register(App, "./App App");
module.hot.reload();
globalThis.__hot_enqueueUpdate && globalThis.__hot_enqueueUpdate();
Note the visitor is incomplete. It only deals with the most basic use case.
I'm a bit confused by the use of
window
. It doesn't look to me as if it is really necessary because the implementation is swapped out? What's the point of that?
@leidegre
I think the implementation in Metro does not use window
but rather the actual global
scope.
I do not know about the original rationale about this implementation, but it has been useful from my experience - it ensures the actual require logic is independent of the fast refresh logic (which means react-refresh/babel
transforms can be used with virtually any bundler). As with the swapping, it also acts as a guard to ensure modules that are not supposed to be processed by the runtime will not be processed:
Consider a case where @babel/runtime
is being used, which will inject helpers as imports to the bundle and you only want to HMR non node_modules
code. If you do not first initialize empty helpers and yet assign helpers to the global scope, a rare case might happen where the Babel-injected helpers will call cleanup
before the user-land module actually finishes initialization (because they are child imports).
Is there a guide to implement it without babel?
So two years down the road, the isLikelyComponentType
did end up causing confusing "hot module not working" issues for end users, just like @theKashey said 😄 Good someone was able to dig up this old thread when researching a Vite HMR issue related to this https://github.com/vitejs/vite/discussions/4583#discussioncomment-1797478
@gaearon I'm trying to implement Fast Refresh into our website, a large application using RequireJS as a module loader. Without something like webpack's hot-reload API, I'm struggling to work out a mechanism to substitute in.
Using a custom TS transformer (we're not using Babel currently) I'm wrapping each define
call in an IIFE which creates a local $RefreshReg$
and $RefreshSig$
, and at the end of the define
function body, I'm calling runtime.register
with the exports
local variable. (Incidentally, I'm not clear how these two $
functions are used, I couldn't see invocations of them anywhere).
For each module modified locally, we have a custom function to create a new RequireJS context, load in the modified module file (including the above transforms) and patch it in by copying exports from the newly loaded module to the original module. After doing that, I'm then calling performReactRefresh
.
And the above works, for the specific module changed. But the refresh does not bubble up, so if I've just changed a file exporting a string, the component which imports that string won't see any changes applied. If we were using Webpack, we would look to see if the newly loaded component was a boundary, and hot.invalidate
if not, bubbling to the parent and calling performReactRefresh
once we hit a boundary.
Figuring out the parent module(s) to bubble to is complex; it would require me to maintain an inverted tree of the whole application in parallel outside RequireJS. But even if I do that, I don't have a way to specifically instruct fast-refresh to mark the parent as needing an update. I could hot-reload the parent so that the module source is re-executed, but that destroys state.
As a "sledgehammer to crack a nut" solution, is there a way to simply mark the whole tree as pending update before calling the performReactRefresh
function?
(Incidentally, I'm not clear how these two $ functions are used, I couldn't see invocations of them anywhere).
The calls are generated by the react-refresh/babel
Babel plugin. Do you have it applied? Without it, nothing else will work.
I could hot-reload the parent so that the module source is re-executed, but that destroys state.
Why does it destroy the state? I don't think it should if the parent component is registered.
@gaearon , @pmmmwh - it's probably a good time officially say goodby to our dear friend React-Hot-Loader. Can you please help me fill some holes to provide a direction for the users still using it in order to migrate to FastRefresh with less friction? 👉 https://github.com/gaearon/react-hot-loader/pull/1848
RHL still has 1M weekly downloads, which is hopefully 1/7th of react-refresh-webpack-plugin
, but that is quite a lot in any case.
Sadly, I am not an JS expert. Trying to migrate away from react-hot-loader.
import { AppContainer } from "react-hot-loader";
ReactDOM.render(
<AppContainer>
What is the suggested upgrade path when moving over to babel with react-refresh (or whatever else is currently the supported solution) https://github.com/pmmmwh/react-refresh-webpack-plugin ?
You dont need to change you runtime to support fast refresh. Just completely remove RHL and then install https://github.com/pmmmwh/react-refresh-webpack-plugin following it's pretty simple and short instructions - update babel config and add webpack plugin.
Dan Abramov mentioned that Devtools v4 will be making
react-hot-loader
obsolete: https://twitter.com/dan_abramov/status/1144715740983046144?s=20I can't see any mention of HMR in the Devtools documentation, however; now that
react-hot-loader
has become obsolete (and with it, therequire("react-hot-loader/root").hot
method), how should we set up apps for HMR in:I'd be particularly interested in a migration guide specifically for anyone who's already set up HMR via
react-hot-loader
.Also, for HMR, does it matter whether we're using the standalone Devtools or the browser-extension Devtools?