reactjs / react-rails

Integrate React.js with Rails views and controllers, the asset pipeline, or webpacker.
Apache License 2.0
6.74k stars 757 forks source link

Compatibility with Turbo (next version of Turbolinks) #1103

Open dvruette opened 3 years ago

dvruette commented 3 years ago

Issue

The next version of Turbolinks, now called Turbo (available for Rails via turbo-rails), is in beta and react-rails should be updated to be compatible. The new interface is similar to the old one, the events are just called differently.

The relevant events are now called turbo:load and turbo:before-render instead of turbolinks:load and turbolinks:before-render. A new event script along the following lines should do the trick:

// in react_ujs/src/events/turbo.js
module.exports = {
  setup: function(ujs) {
    ujs.handleEvent('turbo:load', ujs.handleMount);
    ujs.handleEvent('turbo:before-render', ujs.handleUnmount);
  },

  teardown: function(ujs) {
    ujs.removeEvent('turbo:load', ujs.handleMount);
    ujs.removeEvent('turbo:before-render', ujs.handleUnmount);
  },
}

Along with this some changes to the detection script will be required, but I'm not familiar enough to be able to tell exactly what needs to be added.

Hotfix

I have solved this issue in my app by adding the following two lines to the application.js script:

// earlier: var ujs = require("react_ujs");
ujs.handleEvent('turbo:load', ujs.handleMount);
ujs.handleEvent('turbo:before-render', ujs.handleUnmount);

For anyone already using Turbo and looking to also use react-rails, the above is a hotfix until it's implemented in the package.

Thanks to the contributors for taking a look at this.

phoozle commented 3 years ago

Another thing to consider is turbo-frame events once that is merged https://github.com/hotwired/turbo/pull/59

At the moment React components that come in via a frame do not mount even with the above hotfix. And as per the above PR there is currently no existing events to tap into.

100terres commented 3 years ago

Another thing to consider is turbo-frame events once that is merged hotwired/turbo#59

At the moment React components that come in via a frame do not mount even with the above hotfix. And as per the above PR there is currently no existing events to tap into.

@phoozle They've added turbo:frame-render event https://github.com/hotwired/turbo/pull/327 :slightly_smiling_face:

hbriggs commented 2 years ago

Has anyone gotten this to work recently? I have the latest version (v7.0.1) installed locally and am still unable to render react-rails components in a turbo frame...

dvruette commented 2 years ago

@hbriggs Unfortunately no.

Personally, I’ve started using Stimulus components whenever possible, since they work seamlessly even within turbo frames. It’s a compromise, since Stimulus doesn’t allow for easy DOM manipulation, but I’ve been able to make due so far.

hbriggs commented 2 years ago

@dvruette thanks the for the response! Stimulus is the goal for me too. Unfortunately we have a lot of random react components that we can't immediately migrate so I was hoping to shim them into turbo frames until we have time to do that.

I got the components we were rendering inside of stuff that I moved to a turbo frame to work by doing three things:

  1. I had to declare the props in the controller (hashes sent as props to react_component didn't seem to be rendering as expected within a turbo frame and I got parsing errors; just passing @props solved the parsing issue)
  2. I needed to change some of the links inside of react-rendered components to have target='_top' so that they don't get commandeered by turbo
  3. I added a listener to the frame-load event in my application.js:
    document.addEventListener('turbo:frame-load', function (_e) {
    ReactRailsUJS.mountComponents();
    }, false);

I'm not sure if this is the cleanest way to do this but haven't found anything else so far...

dvruette commented 2 years ago

@hbriggs Nice, thanks for letting me know! Good to know that there is a way now, sometimes I'd prefer to use (or reuse) React components, especially when there are lots of DOM manipulations and Stimulus isn't really a good option.

Would also be great to get this working out of the box, might take a stab at it if I find the time.

lcampanari commented 2 years ago

Got it to work for turbo-frame events:

// app/javascript/packs/application.js

var ReactRailsUJS = require('react_ujs')

ReactRailsUJS.handleEvent('turbo:frame-load', ReactRailsUJS.handleMount)
ReactRailsUJS.handleEvent('turbo:frame-render', ReactRailsUJS.handleUnmount)
MasahiroMorita commented 1 year ago

After installing react-rails and turbo-rails, I encountered the following problem.

Versions and configuration

Problem. 1

After implementing useMediaQuery responsive support, the following exception occurs when changing the screen size.

Uncaught runtime errors:
ERROR
Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
    at removeChildFromContainer (http://localhost:3000/packs/js/vendors-node_modules_hotwired_turbo-rails_app_javascript_turbo_index_js-node_modules_mui_mate-95ab8d.js:66940:15)
    at commitDeletionEffectsOnFiber (http://localhost:3000/packs/js/vendors-node_modules_hotwired_turbo-rails_app_javascript_turbo_index_js-node_modules_mui_mate-95ab8d.js:79856:15)
    at recursivelyTraverseDeletionEffects (http://localhost:3000/packs/js/vendors-node_modules_hotwired_turbo-rails_app_javascript_turbo_index_js-node_modules_mui_mate-95ab8d.js:79819:5)
    at commitDeletionEffectsOnFiber (http://localhost:3000/packs/js/vendors-node_modules_hotwired_turbo-rails_app_javascript_turbo_index_js-node_modules_mui_mate-95ab8d.js:79948:9)
    at commitDeletionEffects (http://localhost:3000/packs/js/vendors-node_modules_hotwired_turbo-rails_app_javascript_turbo_index_js-node_modules_mui_mate-95ab8d.js:79806:5)
    at recursivelyTraverseMutationEffects (http://localhost:3000/packs/js/vendors-node_modules_hotwired_turbo-rails_app_javascript_turbo_index_js-node_modules_mui_mate-95ab8d.js:80089:9)
    at commitMutationEffectsOnFiber (http://localhost:3000/packs/js/vendors-node_modules_hotwired_turbo-rails_app_javascript_turbo_index_js-node_modules_mui_mate-95ab8d.js:80123:9)
    at recursivelyTraverseMutationEffects (http://localhost:3000/packs/js/vendors-node_modules_hotwired_turbo-rails_app_javascript_turbo_index_js-node_modules_mui_mate-95ab8d.js:80103:7)
    at commitMutationEffectsOnFiber (http://localhost:3000/packs/js/vendors-node_modules_hotwired_turbo-rails_app_javascript_turbo_index_js-node_modules_mui_mate-95ab8d.js:80123:9)
    at recursivelyTraverseMutationEffects (http://localhost:3000/packs/js/vendors-node_modules_hotwired_turbo-rails_app_javascript_turbo_index_js-node_modules_mui_mate-95ab8d.js:80103:7)

Problem. 2

The following error occurs with each page transition.

react-dom.development.js:86 Warning: You are calling ReactDOM.unmountComponentAtNode() on a container that was previously passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?

Hotfix for problem 1

The first problem can be avoided by changing the previous snippet as follows:

// Do not call 'handlerMount()' right after the page is first loaded,
// but when the page is subsequently rewritten by turbo-rails.
var skipFirstCall = false
ReactRailsUJS.handleEvent('turbo:load', ()=> {
  skipFirstCall && ReactRailsUJS.handleMount()
  skipFirstCall = true
})

Hotfix for problem 2

The second problem can be avoided by removing the following code:

ReactRailsUJS.handleEvent('turbo:before-render', ReactRailsUJS.handleUnmount)

I don't think this essentially solves the problem, but it solves the glitch anyway. I really hope that you will be able to properly resolve the compatibility issue with turbo-rails.

mattboldt commented 11 months ago

In case anyone runs into this on v3+, since components are no longer automatically unmounted here: https://github.com/reactjs/react-rails/blob/0930f24b0d6869f36b30a5cf853ee92aa9306d20/react_ujs/index.js#L191-L196

I got it to work by calling unmountComponents directly, e.g.:

const componentRequireContext = require.context('components', true)
const ReactRailsUJS = require('react_ujs')
ReactRailsUJS.useContext(componentRequireContext)

// Prevent double mount on page load
ReactRailsUJS._firstTurboLoadSkipped = false
ReactRailsUJS.handleEvent('turbo:load', () => {
  if (ReactRailsUJS._firstTurboLoadSkipped) ReactRailsUJS.handleMount()
  ReactRailsUJS._firstTurboLoadSkipped = true
})

// Unmount components and call cleanup functions after Turbo navigations
ReactRailsUJS.handleEvent('turbo:before-render', (e) => {
  ReactRailsUJS.unmountComponents(e.target)
})