Open Evertt opened 7 months ago
I'm not sure if adding this to #await
is the right move. I think we should instead investigate whether or not we want to have some first-class way to turn a subscribable into a value when called at component initialization. Right now it's pretty easy to do in userland though - you could create your own wrapper which handled pending, error states etc however you want so that you can use the result within an #if-#else
block.
I'm not sure if adding this to #await is the right move.
Why not though? The {#await}
block offers the precise semantic building blocks that one would want for an asynchronous observable that can emit an error.
Also, yes of course with the right wrapper and the {#if}
block you can make everything work. But this:
<script>
import { createQuery } from '@tanstack/svelte-query';
const query = createQuery({
queryKey: ['todos'],
queryFn: () => fetchTodos(),
});
</script>
{#if $query.isLoading}
<p>Loading...</p>
{:else if $query.isError}
<p>Error: {$query.error.message}</p>
{:else if $query.isSuccess}
{#each $query.data as todo}
<p>{todo.title}</p>
{/each}
{/if}
Is just so much more ugly than this:
<script>
import { createQuery } from '@tanstack/svelte-query';
const query = createQuery({
queryKey: ['todos'],
queryFn: () => fetchTodos(),
});
</script>
{#await query}
<p>Loading...</p>
{:then todos}
{#each todos as todo}
<p>{todo.title}</p>
{/each}
{:catch error}
<p>Error: {error.message}</p>
{/if}
While I do like the idea of supporting this natively in {#await}
there is an alternative approach to solving this in userland.
function makePromiseLike<T>(observable: Observable<T>): Observable<T>&PromiseLike<T> {
obs.then = (complete, error) => firstValueFrom(obs).then(complete, error);
return obs;
}
The above function makes any observable Thenable
, which is what SvelteJS checks for: https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/internal/shared/utils.js#L11
Alternatively, just wrap your observable like this:
{#await firstValueFrom(name) }
Waiting for operator name
{:then _}
{ $name }
{/await}
It's still not ideal but definitely workable in my opinion.
I'm not sure if the example given by @dummdidumm is valid. Svelte observables are like RxJs an BehaviorSubject
, they always have a value, so awaiting them is never needed.
@SamMousa I actually did implement something like your makePromiseLike()
function to get it to work in my personal project. I even cheated a little bit, because since promises are always async and SvelteKit SSR does not wait for any async operation, it meant that the {#await promiseLikeObservable}
block would always render the loading block during SSR, even if the data had in fact already been prefetched and could've been rendered during SSR.
I don't exactly remember how I cheated. I either created a makePromiseLike()
function which would check whether the BehaviorSubject
already had anything other than undefined
in its .value
. And if that was the case then my makePromiseLike()
would sneakily make the newly added .then()
method synchronous instead of asynchronous.
But that was fragile, because it worked in some contexts (such as the {#await}
block, but it would result in a runtime error everywhere else where the JS runtime expects a .then()
function to be asynchronous by definition. So then at first I wrote some more super fragile code that would try to interpret from new Error().stack
whether the .then()
was called by Svelte from an {#await}
block. Which was of course super sketchy, because after minification all function names were reduced to just a couple random letters.
In the end I settled on {#await $store || firstValueFrom(store)}
, which worked much more robustly, but was still ugly and not DX friendly at all imo. So still, having Svelte just support this natively would be so nice.
Describe the problem
Context
With Svelte 5's adoption of signals, it's crucial to highlight the complementary role of observables. While signals efficiently handle simple and synchronous state, observables excel in managing complex, asynchronous states without creating spaghetti code.
Problem
Svelte’s current templating syntax offers native support for auto- subscribing and unsubscribing from both their built-in stores and RxJS's observables, but it doesn't offer native support for either the asynchronicity of RxJS's observables nor their error emissions. But Svelte does already offer the
{#await}
block for promises, which could also work perfectly for these observables.Describe the proposed solution
Enhance the
{#await}
block to support observables that are asynchronous and emit errors, bridging the gap between signals and observables for more complex state management.(If you'd like to see the code in
newTypeAheadStore()
you can look at this REPL)Explanation
The searchResults observable initially takes over half a second to emit its first value, displaying a
Loading...
message to the user during this period. Once it emits the first value, a<pre>
block is shown, updating continuously with new results.If an error occurs, particularly during a
fetch
operation, the{:catch}
block is displayed. Post-error, searchResults stops emitting values and requires re-initialization throughnewTypeAhead()
function. Upon re-initialization, the loading message displays again before showing updated results.This concept also applies to Svelte stores. While they may not emit errors, their initial
undefined
state can be interpreted as a loading phase. The loading block remains until the store's state changes fromundefined
, after which the{:then}
block is rendered, without reverting to the loading state, even if the store returns toundefined
.Slightly more advanced version (I'm okay if this doesn't get implemented)
RxJS's observables can also
complete
, after which they will no longer emit values. This is probably not relevant in most cases. But in some cases, we might want to give the user the option to re-subscribe to the observable to get a new stream of values. For that we could implement something like this:Importance
nice to have