sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.85k stars 4.14k forks source link

Await expressions #1857

Open Rich-Harris opened 5 years ago

Rich-Harris commented 5 years ago

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):

<script>
  async function fibonacci(n) {
    return await fibonacciWorker.calculate(n);
  }
</script>

<input type=number bind:value={n}>
<p>The {n}th Fibonacci number is {await fibonacci(n)}</p>

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):

<script>
  import loadImage from './utils.js';
</script>

{#each thumbnails as thumbnail}
  <img alt={thumbnail.description} src="{await loadImage(thumbnail.src)}">
{/each}

Of course, you'd need some way to have placeholder content, for situations where you're not using Suspense. Maybe this?

<p>The {n}th Fibonacci number is {await fibonacci(n) or '...'}</p>
nsivertsen commented 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?

Rich-Harris commented 5 years ago

With the current SSR process, yeah. I'd like to look into ways we could accommodate async/streaming SSR in v3 though

tivac commented 5 years ago

@Rich-Harris Your Suspense link is just a link back to this same issue. 🤔

Rich-Harris commented 5 years ago

D'oh. Fixed

RedHatter commented 5 years ago

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.

frederikhors commented 4 years ago

It would be amazing! Please! :)

ansarizafar commented 4 years ago

Any new update?

rohanrichards commented 4 years ago

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?

dummdidumm commented 3 years ago

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.

Prinzhorn commented 3 years ago

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

Akolyte01 commented 1 year ago

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); }