render_async
render_async copied to clipboard
Content flashes on page revisit even with Turbolinks enabled
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
- draw a loading spinner until the content is returned
- Turbolinks caches the page
- Upon cache restore:
- the content is restored as it was from the Turbolinks cache
- render_async calls the server
- 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?
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?
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?
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?
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.
- Turbolinks restores the page, including the previously fetched
render_async
content -
render_async
activates and removes/placeholders the rendered section -
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 |
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.
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):
- You visit page 1
- render_async loads content
- You visit page 2 and Turbolinks caches page 1 together with content that render_async loaded
- You click on the link to visit page 1
- Turbolinks first shows a preview of the cached version with content that render_async loaded AND makes a request to load the page again
- Page gets loaded and replaces the cached version with the server response (which has the spinner, BTW)
- render_async is triggered again and replaces the spinner
But, if you try to do the following (the Restoration visit):
- You visit page 1
- render_async loads content
- You visit page 2 and Turbolinks caches page 1 together with content that render_async loaded
- You click BACK in the browser to visit page 1
- 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.