Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content flashes on page revisit even with Turbolinks enabled #135

Open
bbugh opened this issue Nov 11, 2020 · 6 comments
Open

Content flashes on page revisit even with Turbolinks enabled #135

bbugh opened this issue Nov 11, 2020 · 6 comments

Comments

@bbugh
Copy link

bbugh commented Nov 11, 2020

Hi! 👋 Very cool gem.

It's quite possible that I'm misunderstanding the intent, so this may not be a bug. Whenever I re-visit a Turbolinks page that has a render_async on it, the render_async runs again (which is great) resulting in an empty space or placeholder loading content (which is not ideal).

The hope was for this particular sometimes slow-loading page that we could

  1. draw a loading spinner until the content is returned
  2. Turbolinks caches the page
  3. Upon cache restore:
    1. the content is restored as it was from the Turbolinks cache
    2. render_async calls the server
    3. if content is different, it's replaced, but otherwise users don't see this

This happens with render_async and render_async_cache.

I made sure to follow the documentation closely, (set the turbolinks option in an initializer, the content_for is in the <head>) but I can't seem to make this work with a fresh Rails 6.0.3.3. or 6.1rc1 app. Am I misunderstanding what should happen here or is this a bug?

@nikolalsvk
Copy link
Owner

Hey, @bbugh. Thanks for opening an issue about this.

About the 1. point on loading a spinner, did you try out an option for setting a placeholder from the docs? Not sure if that helps.

What we do when you set the turbolinks to true in the gem's settings, is that we load render_async on turbolinks:load event. And turbolinks:load event from their docs does the following:

Turbolinks triggers a series of events during navigation. The most significant of these is the turbolinks:load event, which fires once on the initial page load, and again after every Turbolinks visit.

So each time you visit a page, the render_async will listen out for the turbolinks:load event and reload. That is the logic behind it. Not sure if this fits your use-case or helps you somehow?

@bbugh
Copy link
Author

bbugh commented Nov 13, 2020

Thanks for the reply. Yeah, I tried with and without placeholder and neither worked as expected, unfortunately.

It sounds like the current behavior is that render_async will run on every turbolinks load. That's great and what I want to happen. What I don't want to happen is the already-present Turbolinks-restored content removed and replaced with a blank div or the placeholder while the XHR is running. The desired outcome is something like "on first load, use the placeholder content, on future loads just update the DOM differences." If you're familiar with it, we're looking to reproduce Apollo GraphQL's cache-and-network behavior but with Rails.

One possibility I thought of is to tag the top-level element of the sync loader with a data attribute that specifies that it's already been loaded, and have an option for render_async that you can specify "don't remove content when loading if the element has data-loaded". I am not sure if this is feasible, though.

Does this seem like a feature that the render_async code could readily support and/or want?

@nikolalsvk
Copy link
Owner

Ah, I see now. Thanks for explaining in detail.

"on first load, use the placeholder content, on future loads just update the DOM differences."

This sounds like a pretty cool feature we could add. What you described with the data-loaded set to true seems like an easy solution.

But, what I am worried about here is that we need to somehow tell Turbolinks to keep the previous "state" of loaded render_async. The original problem seems to be the flashing of the content on page revisit. This means that we need to cache contents of the loaded render_async somewhere on the frontend so it doesn't show the placeholder (maybe Turbolinks can do this for us with its preview functionality). I'd have to try this out to be sure.

What would be extra helpful is if you can recreate your problem in a small Rails 6 + Turbolinks app. I have one here if you want to do it quickly. I could then play around with it and implement this feature. What do you think?

@bbugh
Copy link
Author

bbugh commented Nov 20, 2020

Thanks for the reply! I will check out your sample project.

Luckily, Turbolinks caches the page right before it draws a new one, not when it's first loaded, so it's correctly caching the content that render_async loads. Turbolinks is caching a copy of the async rendered content, but then render_async activates and removes it.

  1. Turbolinks restores the page, including the previously fetched render_async content
  2. render_async activates and removes/placeholders the rendered section
  3. render_async renders the server result.

I believe that the only required change is to have some way to tell render_async not to remove existing content when it's been loaded once (as an option, presumably), like this matrix:

options.keepExistingContent isAlreadyLoaded
true true do nothing to content, replace (merge?) when complete
true false render placeholder/empty until loaded
false any render placeholder/empty until loaded

@nikolalsvk
Copy link
Owner

Awesome, thanks for providing all this info, it will be valuable for me. I will try to implement this as the next thing. If you find time to recreate a reproducible demo before I fix it, it will be even better.

@nikolalsvk
Copy link
Owner

nikolalsvk commented Dec 13, 2020

Hey, @bbugh, I investigated this issue a bit. Turns out that Turbolinks has 2 types of visits - the Application visit and the Restoration visit. It turns out that the Turbolinks does the following when you click a link (the Application visit):

  1. You visit page 1
  2. render_async loads content
  3. You visit page 2 and Turbolinks caches page 1 together with content that render_async loaded
  4. You click on the link to visit page 1
  5. Turbolinks first shows a preview of the cached version with content that render_async loaded AND makes a request to load the page again
  6. Page gets loaded and replaces the cached version with the server response (which has the spinner, BTW)
  7. render_async is triggered again and replaces the spinner

But, if you try to do the following (the Restoration visit):

  1. You visit page 1
  2. render_async loads content
  3. You visit page 2 and Turbolinks caches page 1 together with content that render_async loaded
  4. You click BACK in the browser to visit page 1
  5. Turbolinks first shows a preview of the cached version with content that render_async loaded

Everything is fine this way because what happened is a Restoration visit and it doesn't request the page from the server like the Application visit does.

So, to sum up, there is not much here to do except to try to override what Turbolinks does, and that is not recommended in their README.

Or we could try to save a previous response body, and wait for turbolinks:load event, put the previous body in the container - but this won't work on all occasions. For example, if you don't set container_id it will get changed on each page visit, and we can't keep track of what to cache.

Anyway, I just wanted to give you an update and document what I found out so far. I might try to add such a feature, but right now it seems too complex. I will keep you posted. Please, let me know if you have any questions or suggestions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants