Open Rich-Harris opened 5 years ago
This would make a lot of things easier, especially around lazy-loading.
One question: This would only ever run on the client, and not in SSR, correct?
With the current SSR process, yeah. I'd like to look into ways we could accommodate async/streaming SSR in v3 though
@Rich-Harris Your Suspense
link is just a link back to this same issue. 🤔
D'oh. Fixed
I really like this. I tend to work with promises a lot and this would make most of my use cases a lot cleaner and easier.
It would be amazing! Please! :)
Any new update?
I was just looking over the docs today for inline awaits. I am predominantly an Angular developer and am used to this syntax in the templates (html)
<div>{{promiseResult | await}}</div>
I'm currently writing some Svelte UI code that right now looks like this:
{#await streamlabsInterface.getSourceNameFromId(node.sourceId) then name}{name}{/await}
Ignoring the over-verbosity of my class/method names, inline await would make this much nicer to work with, are pipes a possibility at all in Svelte?
O more general solution would be to leverage the pipe syntax like rohan suggests. This would make it possible to provide other features besides await and users would be able to provide custom ones themselves.
Moving forward I'd rather see {#await}
being removed than adding more {#await}
. But that's just from my experience and I'm sure there are use-cases for it.
When I started with Svelte I briefly used {#await}
but quickly realized it is way too limited. AbortController
s are now supported in a variety of APIs and libraries. Just "ignoring" a Promise result if it is not longer needed is an antipattern in my opinion. In your original example you will end up with multiple Fibonacci workers eating away your CPU because you never stop them even if the result is no longer needed. If the user quickly clicks the up/down arrow on your [number]
input you keep spawning workers. Unless of course fibonacciWorker.calculate
would be mutually exclusive and handles that, but then you couldn't have two of them on the same page.
Another limitation is that you are forced into the syntax of the {#await}
block. What I mean by that is that for example you can't add a loading
class to a parent. You can only render stuff for the loading state in the given block. Nowhere else.
I personally abstract everything away into stores. Stores are amazing. With everything I mean things like fetch
, Worker
or WebSocket
. For fetch
the store data is an object with loading
, error
and data
(with a default) properties that you can use or not. Here's the Fibonacci example using one of the patterns I'm using a lot:
import { readable } from 'svelte/store';
export const fibonacci = function (n, initialData) {
return readable(
{
loading: true,
error: null,
data: initialData,
},
(set) => {
let controller = new AbortController();
(async () => {
try {
let result = await fibonacciWorker.calculate(n, {
signal: controller.signal
});
set({
loading: false,
error: null,
data: result,
});
} catch (err) {
// Ignore AbortErrors, they're not unexpected but a feature.
// In case of abortion we just keep the loading state because another request is on its way anyway.
if (err.name !== 'AbortError') {
set({
loading: false,
error: err,
data: initialData,
});
}
}
})();
return () => {
controller.abort();
};
}
);
};
<script>
import { fibonacci } from './math.js';
$: result = fibonacci(n, 0);
</script>
<input type=number bind:value={n}>
<p>The {n}th Fibonacci number is {$result.data}</p>
{#if $result.loading}
<p>Show a spinner, add class or whatever you need.</p>
<p>You are not limited to the syntax of an #await block. You are free to do whatever you want.</p>
{/if}
Like I said, that's just from my experience. Maybe Svelte can either offer a way to turn promises into stores or advocate this in user land. I'm using this with great success. No need to debounce fetch
(terrible UX), just fire them away. Aborting them will also cancel the server operations (e.g. in Go you can use https://golang.org/pkg/context/).
Once you've written the imperative library/util code once, your components are super slim and completely reactive/declarative. Wow.
Edit: For people that want the existing semantics (without aborting) but with a store API maybe Svelte could add this:
<script>
import { fromPromise } from 'svelte/store';
$: result = fromPromise(fibonacciWorker.calculate(n), 0);
</script>
<input type=number bind:value={n}>
<p>The {n}th Fibonacci number is {$result.data}</p>
Yes I love stores.
At Square we've followed a similar route as @Prinzhorn in the abundant usage of stores to solve more complicated versions of this problem. We have developed some patterns in the @square/svelte-store package with custom stores that help drastically reduce the amount of boilerplate required to accomplish similar tasks.
I've spun up a suite of examples here: https://codesandbox.io/s/solving-async-problems-with-stores-kr712b
Taking this approach lets you use both #await and state-based conditional rendering, depending on the exact use case. Having access to a promise becomes very useful when you start dealing with more complicated flows of asynchronous data, such as when you want to fetch asynchronous data based on other asynchronous data
However if you don't need this level of control all of this is much heavier than the proposed inline {await}
Something that might additionally be helpful is allowing users to await
promises inside reactive statements. As is you need to do something like this:
$: ((input) => {
fibonacci = await fibonacciWorker.calculate(input);
})(n)
But it would be nice to be able to just do this!
$: { fibonacci = await fibonacciWorker.calculate(n); }
Just want to capture a thought I had the other day: it might be neat to have inline
await
expressions in templates. We already have{#await ...}
blocks but they're overkill in some situations — you have to declare a name for the resolved value, which you might only be using once, and you might not need to worry about error states depending on what you're doing.Imagine something like this (v3 syntax):
It would integrate with Suspense, so it'd be convenient for doing this sort of thing (where
loadImage
resolves to its input, but only after ensuring that the image is loaded):Of course, you'd need some way to have placeholder content, for situations where you're not using Suspense. Maybe this?