React_on_rails: Scroll position lost with Turbolinks

Created on 2 Jun 2016  路  18Comments  路  Source: shakacode/react_on_rails

Hi guys,
i finally managed to migrate to the current version 5 of this gem. Finally, phuuu... Despite the latest versions

gem "react_on_rails", "~> 5"
gem 'turbolinks', '~> 5.0.0.beta'

i'm not able to get turbolinks to work. The problem is when i scroll down a page and navigate from a link down there to a new page and then click back, the previous page is in top position - therefore the scroll position is lost. As far as i understand that's not the way turbolinks 5 should behave.

In my app, the problem is very easy to reproduce. Just create an "empty" react component, let's say like:

render() {
    return (
      <div style={{height: "2000px"}}></div>
    );
  } 

Then load it like always (note the link beneath the component):

<%= react_component('EmptyView', props: { }, :prerender => false ) %>

<a href="/someOtherPage">ClickMe!</a>

Then scroll down the 2000px click the link and click the browser back button. The page is in top position. At least for me. Am i the only one experiencing this problem?

Any help is greatly appreciated.

Thanks in advance

question

Most helpful comment

@justin808 @aaronvb thanks a lot for your help and patience! I saw that this was already fixed when i wanted to start a pull request :D Updating sooner would have saved me some headaches ;)

But at least i learned a lot about ReactOnRails and Turbolinks :D

All 18 comments

@squeezedlight are you sure this is react on rails causing this?

Are you doing server rendering? That might actually solve the problem too.

@jbhatab not 100% right now. But the problem only occurs on pages with react_component(). I tried with and without server side rendering - makes no difference.

Then scroll down the 2000px click the link and click the browser back button. The page is in top position. At least for me. Am i the only one experiencing this problem?

I'm not clear on why scroll position be preserved. Can you point me to the place in the docs? Thanks.

@justin808 sure. The part of the docs i'm referring to is here: https://github.com/turbolinks/turbolinks#restoration-visits

Turbolinks saves the scroll position of each page before navigating away and automatically returns to this saved position on restoration visits.

Thanks for your help!

@SqueezedLight I think we're going to depend on you to do some research on this one.

@justin808 ok! Does that mean you can not reproduce the problem and turbolinks does save scroll position for you? That information would be great help!

@justin808 and @jbhatab
Ok, i tried the following:

===== No React On Rails involved =====
1) Installed rails from scratch - with gem 'turbolinks', '~> 5.0.0.beta'
2) Created two pages to navigate between (/views/application/home.html.erb and /views/application/second.html.erb)
3) Made my tests as described above (with the 2000px div).
--> RESULT: Scroll position is restored.

===== With React On Rails (two different results) =====
I added gem "react_on_rails", "~> 5" and installed the hello_world default. Step by step from the instructions ;)

1) Repeated the tests from before (with /views/application/home.html.erb)
--> RESULT: Scroll position is restored.

2) I set the height of the form on http://localhost:3000/hello_world to a height of 2000px and repeated my tests.
--> RESULT: Scroll position is NOT restored.

In all three cases, i navigated from a link from one site (either home.html.erb or /hello_world) to /second and back.

Any thought on this??

Hi guys, any thoughts on this one??

@SqueezedLight I'm crazy busy right now as we're launching our new product. I'll depend your investigative work!

@justin808 Ok, i just did a quick test. In Turbolinks there is a function called scrollToPosition(). If i set a timeout of 1ms for scrolling everything works as expected. I did it like so:

t.ScrollManager = function() {
    function t(t) {
        this.delegate = t, this.onScroll = e(this.onScroll, this)
    }
    return t.prototype.start = function() {
        return this.started ? void 0 : (addEventListener("scroll", this.onScroll, !1), this.onScroll(), this.started = !0)
    }, t.prototype.stop = function() {
        return this.started ? (removeEventListener("scroll", this.onScroll, !1), this.started = !1) : void 0
    }, t.prototype.scrollToElement = function(t) {
        return t.scrollIntoView()
    }, t.prototype.scrollToPosition = function(t) {
        var e, r;
        console.log(t)
        setTimeout(function(){
          return e = t.x, r = t.y, window.scrollTo(e, r)
        }, 1);
    }, t.prototype.onScroll = function(t) {
        return this.updatePosition({
            x: window.pageXOffset,
            y: window.pageYOffset
        })
    }, t.prototype.updatePosition = function(t) {
        var e;
        return this.position = t, null != (e = this.delegate) ? e.scrollPositionChanged(this.position) : void 0
    }, t
}()

Is it possible that the scrolling happens, before the ReactOnRails Component is set up?

@SqueezedLight We have a couple options:

  1. Update docs with your suggestions
  2. submit a PR to TurboLinks.

I'm not sure how this would be interacting with React on Rails.

@justin808 thanks for your reply. As i described in my tests above, the problem does not appear in a fresh rails installation without ReactOnRails. As far as i can tell now, it definitely has to to with the ReactOnRails gem. If i'm right, a PR to the Turbolinks gem doesn't make sense, does it?

Maybe i should open an issue for Turbolinks to get more opinions on this.

@SqueezedLight, yes, post to the Turbolinks issues, please.

@justin808, i made some progress, but at this point help would definitely be appreciated ;)

Turbolinks saves a copy of the document body with cloneNode(true). But this does not work with react_components. The cloned body contains every single DOM element except the MyView-react-component-0 element and its children.

If Turbolinks is not aware of the ReactOnRails content, restoration visits, and therefore the restoration of the scroll position, can not work.

Help would be appreciated. This seems like a ReactOnRails bug to me.

Thanks a lot!

EDIT: Strange thing: If i call document.querySelector("body").cloneNode(true) from my browser console, everything shows up just fine. Maybe another hint for some kind of race condition??

@SqueezedLight given what you are saying in terms of calling something from the browser console, then yes, we can change the timing of events.

We can check that Turbolinks is installed and v5, and then call what you're calling in the console after the rendering happens.

Here's some pointers of where in the code.

Put in copious print debug statements.

https://github.com/shakacode/react_on_rails/blob/master/node_package/src/clientStartup.js

export function reactOnRailsPageLoaded() {
  debugTurbolinks('reactOnRailsPageLoaded');

  const railsContext = parseRailsContext();
  forEachStore(railsContext);
  forEachComponent(render, railsContext);
}

and


  document.addEventListener('DOMContentLoaded', () => {
    // Install listeners when running on the client (browser).
    // We must do this check for turbolinks AFTER the document is loaded because we load the
    // Webpack bundles first.

    if (!turbolinksInstalled()) {
      debugTurbolinks(
        'NOT USING TURBOLINKS: DOMContentLoaded event, calling reactOnRailsPageLoaded'
      );
      reactOnRailsPageLoaded();
    } else {
      if (turbolinksVersion5()) {
        debugTurbolinks(
          'USING TURBOLINKS 5: document added event listeners turbolinks:before-render and ' +
          'turbolinks:load.'
        );
        document.addEventListener('turbolinks:before-render', reactOnRailsPageUnloaded);
        document.addEventListener('turbolinks:load', reactOnRailsPageLoaded);
      } else {
        debugTurbolinks(
          'USING TURBOLINKS 2: document added event listeners page:before-unload and ' +
          'page:change.');
        document.addEventListener('page:before-unload', reactOnRailsPageUnloaded);
        document.addEventListener('page:change', reactOnRailsPageLoaded);
      }
    }
  });
``

@SqueezedLight Upgrade to > 6.0.0

The problem you're having is fixed in this commit https://github.com/shakacode/react_on_rails/commit/508c06d61a9064dbcbc3c294454a08642f9a9ec0

Prior to React on Rails 6.0.0, the react component unmount function(https://github.com/shakacode/react_on_rails/blob/40553fc58976370ea23d08ce69511b869c4268d9/node_package/src/clientStartup.js#L106) would run BEFORE Turbolinks saved the page to the Turbolinks cache. This means that Turbolinks saved an empty react dom element(which contains the form you're referring to), and that height would be 0px.

In the above commit, the react component unmounting runs AFTER Turbolinks saves the page to the cache.

Edit: In the above repo you linked, I went into the package.json in the client dir and changed the react_on_rails version to 6.0.4 and your above example now works.

Edit 2: If you want to test this without updating(not recommended), go into client/node_modules/react_on_rails/node_package/lib/clientStartup.js and change document.addEventListener('turbolinks:before-cache', reactOnRailsPageUnloaded); to document.addEventListener('turbolinks:before-render', reactOnRailsPageUnloaded);

@justin808 @aaronvb thanks a lot for your help and patience! I saw that this was already fixed when i wanted to start a pull request :D Updating sooner would have saved me some headaches ;)

But at least i learned a lot about ReactOnRails and Turbolinks :D

Was this page helpful?
0 / 5 - 0 ratings