Skip to content

Commit

Permalink
Add React Fast Refresh #973
Browse files Browse the repository at this point in the history
  • Loading branch information
onigoetz committed Aug 17, 2022
1 parent 2d64515 commit 56ca66c
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 12 deletions.
59 changes: 51 additions & 8 deletions packages/crafty-preset-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

- [Babel](05_Packages/05_crafty-preset-babel.md)
- [TypeScript](05_Packages/05_crafty-preset-typescript.md)
- [SWC](05_Packages/05_crafty-preset-swc.md)
- [Jest](05_Packages/05_crafty-preset-jest.md)

</td></tr>
Expand All @@ -31,16 +32,17 @@ npm install @swissquote/crafty-preset-babel --save
```javascript
module.exports = {
presets: [
"@swissquote/crafty-preset-babel", // also works with crafty-preset-typescript
"@swissquote/crafty-runner-webpack", // optional
// also works with crafty-preset-typescript or crafty-preset-swc
"@swissquote/crafty-preset-babel",
"@swissquote/crafty-runner-webpack",
"@swissquote/crafty-preset-react"
],
js: {
app: {
runner: "webpack",
source: "js/app.js",
hot: true, // Hot Module Replacement must be enabled for React Hot Loader to work
react: true // React Hot Loader must be explicitly enabled in your bundle
react: true // React features must be enabled per bundle
}
}
};
Expand All @@ -56,12 +58,11 @@ When doing modern JavaScript development, the usual process is **Write code**,
part.

More precisely, when doing a build with Webpack, in development mode, a
Websocket client is added to the build and a small HTTP server is started. When
the page is loaded, each bundle will establish a Websocket connection to the
server.
Websocket client is added to the build and an HTTP server is started.
When the page is loaded, each bundle will establish a Websocket connection to the server.

When you change a line of code, the server will rebuild them and send a
notification through Websockets to the browser, the browser will then download
notification through Websocket to the browser, the browser will then download
the patch and apply the code change.

With React components, it will even re-render them without losing the current
Expand All @@ -71,7 +72,49 @@ Here's an example :

![React Hot Module Replacement example](../react-hot-loader.gif)

To enable HMR on your react application, you must set `hot: true` and `react: true` on your bundle in `crafty.config.js`.
## React Hot Module Replacement variants

There are two ways to make this work

### Fast Refresh

> This variant will work when using the Babel, TypeScript and SWC presets with Webpack.
Starting with React 16.13, [Fast Refresh](https://www.npmjs.com/package/@pmmmwh/react-refresh-webpack-plugin) is the way to do Hot Module Replacement and doesn't require to add code to your application to get it to work.
This is the recommended way and will be the default starting with Crafty 1.20.0

To enable it, add these two parameters to your bundle in `crafty.config.js`:

```js
{
hot: true,
react: true,
// also works with
react: {
refreshMode: "fast"
}
}
```

You're now ready to run `crafty watch` and use Fast Refresh on all your components.


### React Hot Loader

> This variant will work when using the Babel or TypeScript presets with Webpack.
Most versions of React can work with [React Hot Loader](https://www.npmjs.com/package/react-hot-loader), but in some cases can't keep the component's state on refresh or don't support hooks, it's a deprecated way to do and will be removed from Crafty in the future.

To enable it, add these two parameters to your bundle in `crafty.config.js`:

```js
{
hot: true,
react: {
refreshMode: "hot"
}
}
```

Then you must mark your root component as hot-exported :

Expand Down
71 changes: 70 additions & 1 deletion packages/crafty-preset-react/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,38 @@ function enableHotLoader(crafty, bundle) {
crafty.getEnvironment() === "development" &&
crafty.isWatching() &&
bundle.hot &&
bundle.react
bundle.react &&
bundle.react.refreshMode === "hot"
);
}

function enableFastRefresh(crafty, bundle) {
return (
crafty.getEnvironment() === "development" &&
crafty.isWatching() &&
bundle.hot &&
bundle.react &&
bundle.react.refreshMode === "fast"
);
}

module.exports = {
normalizeBundle(crafty, bundle) {
if (!bundle.react) {
return;
}

// Initially "react" was a boolean configuration
// since we are moving from "hot reload" to "fast refresh"
// the goal is to make it clear what option we're choosing
// but still allow to pick a single boolean value to simplify
// the configuration
if (bundle.react && typeof bundle.react === "boolean") {
bundle.react = {
refreshMode: "fast"
};
}
},
jest(crafty, options) {
options.moduleDirectories.push(MODULES);

Expand All @@ -21,8 +48,29 @@ module.exports = {
babel(crafty, bundle, babelConfig) {
// Add hot module replacement for bundles with React
if (enableHotLoader(crafty, bundle)) {
console.log("Enabled hot loader");
babelConfig.plugins.push(require.resolve("react-hot-loader/babel"));
}

// Add fast refresh for bundles with React
if (enableFastRefresh(crafty, bundle)) {
console.log("Enabled fast refresh");
babelConfig.plugins.push(require.resolve("react-refresh/babel"));
}
},
swc(crafty, bundle, swcOptions) {
if (enableFastRefresh(crafty, bundle)) {
if (!swcOptions.jsc.transform) {
swcOptions.jsc.transform = {};
}

if (!swcOptions.jsc.transform.react) {
swcOptions.jsc.transform.react = {};
}

swcOptions.jsc.transform.react.development = true;
swcOptions.jsc.transform.react.refresh = true;
}
},
webpack(crafty, bundle, chain) {
// Resolve this module for Yarn PNP
Expand All @@ -41,5 +89,26 @@ module.exports = {
.use("react-hot-loader")
.loader(require.resolve("react-hot-loader/webpack"));
}

if (enableFastRefresh(crafty, bundle)) {
// If somebody still includes react-hot-loader,
// make sure they know they should remove it
// but doesn't impact production
chain.resolve.alias.set(
"react-hot-loader",
path.dirname(require.resolve("./react-hot-loader"))
);

// https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/API.md#reactrefreshpluginoptions
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");

chain.plugin("react-refresh").use(ReactRefreshWebpackPlugin, [
{
overlay: {
sockIntegration: "wps" // webpack-plugin-serve
}
}
]);
}
}
};
4 changes: 3 additions & 1 deletion packages/crafty-preset-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"lint": "node ../crafty-preset-eslint/src/commands/jsLint.js --preset recommended --preset node '*.js'"
},
"dependencies": {
"react-hot-loader": "4.13.0"
"@pmmmwh/react-refresh-webpack-plugin": "0.5.7",
"react-hot-loader": "4.13.0",
"react-refresh": "0.14.0"
},
"engines": {
"node": ">=12"
Expand Down
16 changes: 16 additions & 0 deletions packages/crafty-preset-react/react-hot-loader/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use strict";

if (process.env.NODE_ENV === "production") {
module.exports = require("./production.js");
} else if (process.env.NODE_ENV === "test") {
module.exports = require("./production.js");
} else if (typeof window === "undefined") {
// this is just server environment
module.exports = require("./production.js");
} else if (!module.hot) {
module.exports = require("./production.js");
} else {
console.warn("You seem to import 'react-hot-loader' in your application, but are using react-fast-refresh. You don't need these imports anymore.");

module.exports = require("./production.js");
}
22 changes: 22 additions & 0 deletions packages/crafty-preset-react/react-hot-loader/production.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";

export function AppContainer(e) {
return React.Children.only(e.children);
}
export function hot() {
return function(e) {
return e;
};
}
export function areComponentsEqual(e, n) {
return e === n;
}
export function setConfig() {
// NOOP
}
export function cold(e) {
return e;
}
export function configureComponent() {
// NOOP
}
5 changes: 5 additions & 0 deletions packages/crafty/src/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ function registerTasks(crafty) {
bundle.destination = `${bundleName}.min.js`;
}

crafty.getImplementations("normalizeBundle").forEach(preset => {
debug(`${preset.presetName}.normalizeBundle(crafty, bundle)`);
preset.normalizeBundle(crafty, bundle);
});

if (
!hasOwnProperty.call(bundleCreators, type) ||
Object.keys(bundleCreators[type]).length === 0
Expand Down
Loading

0 comments on commit 56ca66c

Please sign in to comment.