React component not rendering correctly with Turbolinks in Rails 5.1

I have a very simple Rails application with a responsive component that simply displays "Hello" in an existing div on a specific page (say, a show page).

When I load the linked page using the url it works. I see Hello on the page.

However, when I am previously on another page (say the index page and then I go to the show page with Turbolinks, well, the component is not displayed unless I go back over and over again. The index page and return to the show page)

From here, every time I go back and forth, I can tell that the view is rendered twice as much.
Not only twice, but twice the time! (i.e. 2 times, then 4, then 6, etc.)

I know that since I was installing content at the same time div

, I was printing a message to the console.

In fact, I think that returning to the index page should still run the component code without displaying, since the div element is not on the index page. But why together?

I want to solve the following problems:

  • To run the code on the first request of the show page
  • To block code from running on other pages (including the index page)
  • To have the code run once on subsequent requests for the show page

Here are the exact steps and code I used (I'll try to be as concise as possible).

  • I have a Rails 5.1 application with a permission set with:

    rails new myapp --webpack=react
    
          

  • Then I create a simple elemental sketch to get some pages to play:

    rails generate scaffold Item name
    
          

  • I'll just add the following div element to the Show ( app/views/items/show.html.erb

    ) page :

    <div id=hello></div>
    
          

  • Webpacker already generated the Hello ( hello_react.jsx

    ) component , which I modified as indicated in the order of using the above div element. I changed the original event 'DOMContentLoaded'

    :

    document.addEventListener('turbolinks:load', () => {
      console.log("DOM loaded..");
      var element = document.getElementById("hello");
      if(element) {
        ReactDOM.render(<Hello name="React" />, element)
      }
    })
    
          

  • Then I added the following webpack script tag at the bottom of the previous view ( app/views/items/show.html.erb

    ):

    <%= javascript_pack_tag("hello_react") %>
    
          

  • Then I run rails server

    and webpack-dev-server

    using foreman start

    (installed by adding gem 'foreman'

    to Gemfile

    ). Here is the content Procfile

    I used:

    web:     bin/rails server -b 0.0.0.0 -p 3000
    webpack: bin/webpack-dev-server --port 8080 --hot
    
          


And here are the next steps to reproduce the described behavior:

  • Load index page using url http://localhost:3000/items

  • Click New Item to add a new item. Rails redirects to the page showing products by URL localhost:3000/items/1

    . Here we see Hello React! message. It works well!
  • Load the index page using the URL http://localhost:3000/items

    . The item is displayed as expected.
  • Load the show page using the URL http://localhost:3000/items/1

    . The Hello message is displayed as expected with a single console message.
  • Load index page using url http://localhost:3000/items

  • Click on the "Show" link (must be done via turbolink). The message is not displayed in any console message.
  • Click the Back link (must be done via turbolink) to go to the index page.
  • Click the "Show" link again (to be done via turbolink). This time the message is displayed well. The console message for its part is shown twice .

From there, every time I go back to the index and return to the show page again, two more messages are displayed on the console each time.

Note. Instead of using (and replacing) a specific div element, if I allow the original file hello_react

that adds the div element, this behavior becomes even more noticeable.
Edit: Also, if I change the links link_to

to include data: {turbolinks: false}

. It works well. Just like we loaded pages using URLs in the browser's address bar.


I don't know what I am doing wrong.
Any ideas?

Edit: I've put the code in the following repo if you're interested in trying it: https://github.com/sanjibukai/react-turbolinks-test

+3


source to share


2 answers


This is a pretty tricky problem and I'm afraid I don't think it has a straightforward answer. I will explain as far as possible.

  • To run the code on the first request of the show page

Your event handler has turbolinks:load

n't been fired because your code is fired after the event has fired turbolinks:load

. Here is the stream:

  • User views the page
  • turbolinks:load

    works
  • Script in Expressed Body

This way, the event handler turbolinks:load

won't get called (and therefore your React component won't render) until the next page loads.

To (partially) solve this problem, you can remove the event listener turbolinks:load

and call render

directly:

ReactDOM.render(
  <Hello name="React" />,
  document.body.appendChild(document.createElement('div'))
)

      

Alternatively, you can use <%= content_for … %>

/ <%= yield %>

to insert a script tag in head

. for example in your layout application.html.erb

<head>
  <%= yield :javascript_pack %>
</head>

      

then in your show.html.erb:



<%= content_for :javascript_pack, javascript_pack_tag('hello_react') %>

      

In both cases, it costs nothing that for any HTML you add to a page with JavaScript in a block turbolinks:load

, you must remove it on turbolinks:before-cache

to prevent duplicate issues when re-viewing pages. In your case, you can do something like:

var div = document.createElement('div')

ReactDOM.render(
  <Hello name="React" />,
  document.body.appendChild(div)
)

document.addEventListener('turbolinks:before-cache', function () {
  ReactDOM.unmountComponentAtNode(div)
})

      

Even so, you may run into duplicate issues when re-viewing pages. I believe this has to do with how the previews are viewed, but I was unable to fix this without disabling the previews.

  • To have the code run once on subsequent requests for the show page
  • To block code from running on other pages (including the index page)

As I mentioned above, including page-related scripts dynamically can be difficult when using Turbolinks. Event listeners in a Turbolinks application behave differently than they do without Turbolinks, where each page gets a new document

one and therefore the event listeners are removed automatically. Unless you manually remove the event listener (e.g. on turbolinks:before-cache

), each visit to this page will add another listener. What's more, if Turbolinks is turbolinks:load

caching this page, the event will fire twice: once for the cached version and another for the new copy. This is probably because you've seen it 2, 4, 6 times.

With this in mind, my best advice is to avoid adding page-specific scripts to run page-specific code. Instead, include all of your scripts in your application.js manifest file and use the elements on your page to determine if the component mounts. Your example does something like this in the comments:

document.addEventListener('turbolinks:load', () => {
  var element = document.getElementById("hello");
  if(element) {
    ReactDOM.render(<Hello name="React" />, element)
  }
})

      

If this is included in your application.js, then any page with an element #hello

will get a component.

Hope it helps!

+3


source


After what you said, I checked some code.


First, I just pulled the methodReactDOM.render

out of your listener as you suggested in your first snippet.
This is a big leap forward since the message is no longer displayed elsewhere (for example, on the index page), but only on the display page if required.

But something interesting is happening on the show page. no more piling up the message as an added div element, which is good. In fact, it even rendered once as needed. But .. Console message is displayed twice !?

I'm guessing there is something going on here related to the caching mechanism, but since the message has to be added , why isn't it shown twice as a console message?

Putting this issue aside, it seems to work, and I'm wondering why is this necessary in the first place for rendering React after page load (without Turbolinks, there was an event listener DOMContentLoaded

)?
I am guessing this is due to the unexpected rendering of the javascript code being executed when some DOM elements have not been loaded yet.


Then I tried an alternative way using <%= content_for … %>

/<%= yield %>

. And as you'd expect, it will give mitigating results and some strange behavior.

When I load the index page via the URL page and then navigate to the show page with Turbolink, it works!
The div message as well as the console message are displayed once .
Then if I go back (using Turbolink) the div message disappears and I got the console message ".. unmounted .." if you like.

But since then, whenever I go back to the show page, the div and console message are never shown at all .
The only message that is displayed is the console message ".. unmounted .." when I return to the index page.

Even worse, if I load the show page using the url, the div message is no longer !? The console message is displayed, but I got an error regarding the div ( Cannot read property 'appenChild' of null

) element . I will not deny that I completely ignore what is happening here.




Finally, I tried your last best advice and just put the last piece of code in the HTML head .

Since this is code jsx

, I don't know how to handle it in the pipeline / structure of the Rails file, so I put mine javascript_pack_tag

in html head

. Indeed, it works well .

This time the code is executed everywhere, so it makes sense to use a page specific element (as previously suggested in the commented code).

The downside is that the code can get messy this time if I don't put all the page- if

specific code inside statements that check for a page-specific element.
However, since Rails / Webpack has a good code structure, it should be easy to manage to put the page specific code in files jsx

for the page.

However, the advantage is that this time, all parts related to the page will be displayed at the same time as the entire page, thus avoiding the display glitch that would otherwise occur.

I didn't address this issue in the first place, but I really would like to know how to get the page content displayed at the same time as the entire page.
I don't know if this is possible when combining Turbolink with React (or any other framework).


But in conclusion, I leave this question later.

Thanks for your input Dom ..

+2


source







All Articles