sapper
sapper copied to clipboard
Feature Request: Hydratable {#await} blocks
Is your feature request related to a problem? Please describe.
When using {#await}, Sapper doesn't seem to wait at all for the result of the promise, and always renders the the placeholder part.
In the below example, Hello darkness. initially appears on the page, and is then replaced by Hello world., potentially confusing the visitor.
Hello
{#await Promise.resolve("world")}
darkness.
{:then result}
{result}.
{/await}
Describe the solution you'd like It would be amazing if Sapper was able to resolve the promises server-side so that the browser doesn't even need to execute them again.
I'm currently thinking how this behavior could be implemented, or an elegant work-around. No ideas so far...
This isn't possible with Svelte in its current form, because SSR is always synchronous. This is part of what preload is for - it lets you run asynchronous code before the component is even instantiated.
Right. I'm just thinking about how to make common use cases easier to implement. Consider this (I think rather common case):
- A search box and results are displayed on the page
- Whenever the user inputs a new
searchTerm, a loading animation should start playing. - When the
searchurl query parameter is present, it should server-side render the results for that search term for better SEO.
My current solution works but feels really awkward.
<script context="module">
async function fetchData(searchTerm) {
return Promise.resolve('Results for ' + searchTerm)
}
export async function preload(page) {
return {
results: await fetchData(page.query.search)
};
}
</script>
<script>
import {stores} from '@sapper/app';
const {page} = stores();
export let results;
let searchTerm = $page.query.search;
// If we run in SSR mode, we don't want to start an async process
$: results = typeof window == 'undefined' ? results : fetchData(searchTerm)
</script>
<input bind:value={searchTerm} type="text">
{#await results}
Nice loading animation
{:then data}
{data}
{/await}
I was hoping to get this down to:
<script>
import {stores} from '@sapper/app';
const {page} = stores();
let searchTerm = $page.query.search;
</script>
<input bind:value={searchTerm} type="text">
{#await fetchData(searchTerm)}
Nice loading animation
{:then data}
{data}
{/await}
This just feels like it should be possible.
I wonder if it's possible to modify the SSR compiler to produce async code. So that the produced code actually executes all promises it encountered in {#await} blocks, and uses the results to render the page.
The entire render path would need to be async 🤷
Routify has a way to make this work using the $ready() function. Maybe Sapper could allow implement similar? You know, just 2 ways of declaring when you finish SSR and send the results to the browser?
I wonder if it's possible to modify the SSR compiler to produce async code. So that the produced code actually executes all promises it encountered in {#await} blocks, and uses the results to render the page.
I would want to go a slightly different route. In my use-case the promise that I'm awaiting is indeed already resolved, just like in your example. As in I made sure that the promise got resolved in the preload() function. So sapper would literally only need to "wait" one clock tick in order to get the results in the {#await} block.
So I would just want to ask Sapper to wait 1 clock tick if it happens to find an {#await} block in the template.
@Conduitry is that more realistic to implement?
@Conduitry I was running up against this problem again and in my search for a solution I found this:
https://github.com/Yukaii/synchronized-promise
With that package, you can wait for a promise synchronously. And you can set how long you're willing to wait for the promise to resolve.
// A promise that's already resolved
const promise = Promise.resolve("Already resolved")
try {
// Get the already-resolved value synchronously
const value = sp(() => promise, { tick: 1, timeouts: 3 })()
} catch (err) {
// Here you can check if the err came from the timeout,
// in which case you'd render the "loading" part of the await block.
// Otherwise you can render the "error" part of the await block.
}
In that example I'm saying it should check every 1 millisecond and wait max 3 milliseconds. That should normally work for promises that are already resolved, I think? That way Sapper, or Sveltekit, can prerender {#await} blocks whose promises have already resolved without suffering significant performance issues or needing to rewrite everything to be async.
I don't think that's a good solution because the package relies on deasync which is node runtime dependent, so that may very well not work in all environments.
Why not? Doesn't both Sapper and Sveltekit both run on node on the server side? Are there any plans to make them work in other environments?
SvelteKit does not necessarily run in a node environment, and even if, it's not only the node version but also the operating system that affects deasync.
Ah okay, that's too bad then