Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,38 @@ LibV8 itself is already [beyond version 7](https://github.com/cowboyd/libv8/rele
### HMR
Hot Module Replacement is [possible with this gem](https://stackoverflow.com/a/54846330/193785) as it does just pass through to Webpacker. Please open an issue to let us know tips and tricks for it to add to the wiki.

Sample repo that shows HMR working with `react-rails`: [https://github.com/edelgado/react-rails-hmr](https://github.com/edelgado/react-rails-hmr)
Similar to `useContext`, you can pass the webpack context into `useHotReload` to enable hot reload:
1. install [react-hot-loader](https://github.com/gaearon/react-hot-loader) and [@hot-loader/react-dom](https://github.com/hot-loader/react-dom)
2. add the following to your webpack config in dev:
```js
{
module: {
rules: [
{
test: /\.(jsx|tsx)?$/,
use: ["react-hot-loader/webpack"],
},
],
},
resolve: {
alias: {
"react-dom": "@hot-loader/react-dom",
},
},
}
```
3. in your entry file, usually where you call `ReactRailsUJS.useContext` already, call `useHotReload`:
```js
var ReactRailsUJS = require("react_ujs")
var myCustomContext = require.context("custom_components", true)
ReactRailsUJS.useHotReload(myCustomContext)
```
4. optionally, for CSS to hot reload, update the following for dev in `webpacker.yml`:
```yml
development:
<<: *default
extract_css: false
```
Comment on lines +687 to +717
Copy link
Author

@EdmondChuiHW EdmondChuiHW Apr 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if the steps in readme is concise and simple to follow. Happy to pull into a separate file if requested


One caveat is that currently you [cannot Server-Side Render along with HMR](https://github.com/reactjs/react-rails/issues/925#issuecomment-415469572).

Expand Down
25 changes: 23 additions & 2 deletions react_ujs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ var detectEvents = require("./src/events/detect")
var constructorFromGlobal = require("./src/getConstructor/fromGlobal")
var constructorFromRequireContextWithGlobalFallback = require("./src/getConstructor/fromRequireContextWithGlobalFallback")

var renderWithReactDOM = require("./src/renderComponent/withReactDOM")
var renderWithHotReload = require("./src/renderComponent/withHotReload")
Comment on lines +9 to +10
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the add render component extension point PR, the folder structure and import pattern is taking reference from getConstructor for consistency. Please let me know if there is a better way


var ReactRailsUJS = {
// This attribute holds the name of component which should be mounted
// example: `data-react-class="MyApp.Items.EditForm"`
Expand Down Expand Up @@ -71,6 +74,11 @@ var ReactRailsUJS = {
useContext: function(requireContext) {
this.getConstructor = constructorFromRequireContextWithGlobalFallback(requireContext)
},

// Called after React unmounts component at `node`
// Override this function to perform any cleanup
// the default function does nothing
onComponentUnmountAtNode: function (node) {},

// Render `componentName` with `props` to a string,
// using the specified `renderFunction` from `react-dom/server`.
Expand All @@ -80,6 +88,18 @@ var ReactRailsUJS = {
return ReactDOMServer[renderFunction](element)
},

// Render `component` using the specified `renderFunction` from `react-dom`.
// Override this function to render components in a custom way.
// function signature: ("hydrate" | "render", component, node, props)
renderComponent: renderWithReactDOM,

// Enables hot reload for component rendering.
//
// See the HMR section in README to ensure required steps are completed.
useHotReload: function(requireContext) {
this.renderComponent = renderWithHotReload(requireContext)
},
Comment on lines +96 to +101
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if this comment could be more helpful, i.e. should the steps to setup be inlined here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, there is an opportunity to "stack" instead of "replace". See the example and discussion at #1064. The proposed approach here mirrors getConstructor and useContext. Let me know if the "stack" option would be a better option


// Within `searchSelector`, find nodes which should have React components
// inside them, and mount them with their props.
mountComponents: function(searchSelector) {
Expand Down Expand Up @@ -112,9 +132,9 @@ var ReactRailsUJS = {
}

if (hydrate && typeof ReactDOM.hydrate === "function") {
component = ReactDOM.hydrate(component, node);
renderComponent("hydrate", component, node, props);
} else {
component = ReactDOM.render(component, node);
renderComponent("render", component, node, props);
}
}
}
Expand All @@ -128,6 +148,7 @@ var ReactRailsUJS = {
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
ReactDOM.unmountComponentAtNode(node);
ReactRailsUJS.onComponentUnmountAtNode(node);
}
},

Expand Down
64 changes: 64 additions & 0 deletions react_ujs/src/renderComponent/withHotReload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
var ReactDOM = require("react-dom")
var reactHotLoader = require("react-hot-loader")
var AppContainer = reactHotLoader.AppContainer;

var IS_MOUNTED_ATTR = "data-react-is-mounted";

// Render React component with hot reload.
//
// See the HMR section in README to ensure required steps are completed.
Comment on lines +7 to +9
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this one. Let me know if this comment could be more helpful, i.e. should the steps to setup be inlined here?

module.exports = function(webpackRequireContext) {
ReactRailsUJS.onComponentUnmountAtNode = function(node) {
node.setAttribute(IS_MOUNTED_ATTR, "false");
}

return function(renderFunctionName, component, node, props) {
var className = node.getAttribute(ReactRailsUJS.CLASS_NAME_ATTR);
var filename = getFileNameFromClassName(className);
var path = webpackRequireContext.resolve("./" + filename);
var cache = require.cache;
var module = cache[path];
var moduleParent = module && cache[module.parents[0]];
if (!moduleParent || !moduleParent.hot) {
console.warn(`Cannot hot reload for ${path}. Ensure webpack-dev-server is started with --hot and WEBPACKER_DEV_SERVER_HMR=true`);
return;
}
moduleParent.hot.accept(path, () => reRenderAllNodes(className, renderFunctionName));

ReactDOM[renderFunctionName](React.createElement(AppContainer, null, component), node);
node.setAttribute(IS_MOUNTED_ATTR, "true");
};
}

function getFileNameFromClassName(className) {
var parts = className.split(".");
var filename = parts.shift();

return filename;
}

function reRenderAllNodes(className, renderFunctionName) {
var nodes = findAllReactNodes(className);
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
if (!isReactMountedAtNode(node)) continue;

var propsJson = node.getAttribute(ujs.PROPS_ATTR);
var props = propsJson && JSON.parse(propsJson);
var FreshComponent = React.createElement(FreshConstructor, props);
ReactDOM[renderFunctionName](React.createElement(AppContainer, null, FreshComponent), node);
}
}

function findAllReactNodes(className) {
var selector = '[' + ReactRailsUJS.CLASS_NAME_ATTR + '="' + className + '"]';
if (ReactRailsUJS.jQuery) {
return ReactRailsUJS.jQuery(selector, document);
} else {
return parent.querySelectorAll(selector);
}
}

function isReactMountedAtNode(node) {
return node.matches('[' + IS_MOUNTED_ATTR + '="true"]');
}
10 changes: 10 additions & 0 deletions react_ujs/src/renderComponent/withReactDOM.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
var ReactDOM = require("react-dom")

// Render React component via ReactDOM, for example:
//
// - `renderComponent("hydrate", component, node, props)` -> `ReactDOM.hydrate(component, node);`
// - `renderComponent("render", component, node, props)` -> `ReactDOM.render(component, node);`
//
module.exports = function(renderFunctionName, component, node) {
ReactDOM[renderFunctionName](component, node);
};