sapper icon indicating copy to clipboard operation
sapper copied to clipboard

Feature Request: Hydratable {#await} blocks

Open buhrmi opened this issue 6 years ago • 11 comments
trafficstars

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...

buhrmi avatar Oct 21 '19 16:10 buhrmi

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.

Conduitry avatar Oct 21 '19 16:10 Conduitry

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 search url 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.

buhrmi avatar Oct 21 '19 19:10 buhrmi

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.

buhrmi avatar Oct 21 '19 20:10 buhrmi

The entire render path would need to be async 🤷

buhrmi avatar Oct 22 '19 00:10 buhrmi

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?

Evertt avatar Sep 19 '20 11:09 Evertt

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?

Evertt avatar Sep 20 '20 12:09 Evertt

@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.

Evertt avatar Mar 17 '21 12:03 Evertt

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.

dummdidumm avatar Mar 17 '21 14:03 dummdidumm

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?

Evertt avatar Mar 17 '21 14:03 Evertt

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.

dummdidumm avatar Mar 17 '21 14:03 dummdidumm

Ah okay, that's too bad then

Evertt avatar Mar 17 '21 14:03 Evertt