Svelte: Async reactive declarations

Created on 21 Feb 2019  ยท  14Comments  ยท  Source: sveltejs/svelte

Async reactive declaration that returns resolved promise would be a nice addition and sufficiently important If you are using a function form a third party module which returns a promise. This feature has also been discussed in the Support channel recently.

pending clarification

Most helpful comment

For anyone who's stumbled across this issue in search for asynchronous reactive declarations (like me) โ€” here's two ways to achieve them in Svelte:

Reactive Statements

let package_name = 'svelte';
let download_count = 0;
$: fetch('https://api.npmjs.org/downloads/point/last-week/' + package_name)
    .then(response => response.json())
    .then(data => download_count = data.downloads || 0);

// Updating `package_name` will asynchronously update `download_count`

Pros & Cons

  • โœ… Very terse
  • โš ๏ธ Handling race conditions (e.g. an earlier fetch() finishing after a later one, thus overriding download_count with an outdated value) would require an extra helper variable and make the reactive statement significantly longer.
  • โš ๏ธ Kind of brittle through imperative instead of declarative assignment: Nobody prevents you from defining multiple places where download_count is changed, possibly leading to even more race conditions.

More Information

Derived Stores

import { writable, derived } from 'svelte/store';

const package_name = writable('svelte');
const download_count = derived(
    package_name,
    ($package_name, set) => {
        fetch('https://api.npmjs.org/downloads/point/last-week/' + $package_name)
            .then(response => response.json())
            .then(data => set(data.downloads));

        return () => {
            // We override the `set` function to eliminate race conditions
            // This does *not* abort running fetch() requests, it only prevents
            // them from overriding the store.
            // To learn about canceling fetch requests, search the internet for `AbortController`
            set = () => {}
        }
    }
);

// Updating `$package_name` will asynchronously update `$download_count`

Pros & Cons

Pretty much the opposite of the "reactive statements" approach:

  • โœ… Eliminates race conditions
  • โœ… The derived callback is the single source of truth
  • โš ๏ธ Slightly more boilerplate

More Information


@Conduitry I think this topic may deserve its own section in the docs. All the ways to achieve this are properly documented, but I'd think quite some people are actually searching the docs for the "async reactive declarations" keyword and currently not finding anything.

All 14 comments

I'm not sure what this would entail. You can still .then promises and update things asynchronously inside a reactive declaration block. If the desire is to allow an await inside the block without a wrapping async IIFE, that's not going to be possible, because we do need the input to be parseable as vanilla javascript.

For anyone who's stumbled across this issue in search for asynchronous reactive declarations (like me) โ€” here's two ways to achieve them in Svelte:

Reactive Statements

let package_name = 'svelte';
let download_count = 0;
$: fetch('https://api.npmjs.org/downloads/point/last-week/' + package_name)
    .then(response => response.json())
    .then(data => download_count = data.downloads || 0);

// Updating `package_name` will asynchronously update `download_count`

Pros & Cons

  • โœ… Very terse
  • โš ๏ธ Handling race conditions (e.g. an earlier fetch() finishing after a later one, thus overriding download_count with an outdated value) would require an extra helper variable and make the reactive statement significantly longer.
  • โš ๏ธ Kind of brittle through imperative instead of declarative assignment: Nobody prevents you from defining multiple places where download_count is changed, possibly leading to even more race conditions.

More Information

Derived Stores

import { writable, derived } from 'svelte/store';

const package_name = writable('svelte');
const download_count = derived(
    package_name,
    ($package_name, set) => {
        fetch('https://api.npmjs.org/downloads/point/last-week/' + $package_name)
            .then(response => response.json())
            .then(data => set(data.downloads));

        return () => {
            // We override the `set` function to eliminate race conditions
            // This does *not* abort running fetch() requests, it only prevents
            // them from overriding the store.
            // To learn about canceling fetch requests, search the internet for `AbortController`
            set = () => {}
        }
    }
);

// Updating `$package_name` will asynchronously update `$download_count`

Pros & Cons

Pretty much the opposite of the "reactive statements" approach:

  • โœ… Eliminates race conditions
  • โœ… The derived callback is the single source of truth
  • โš ๏ธ Slightly more boilerplate

More Information


@Conduitry I think this topic may deserve its own section in the docs. All the ways to achieve this are properly documented, but I'd think quite some people are actually searching the docs for the "async reactive declarations" keyword and currently not finding anything.

@loilo's example is great, but is still missing progress and errors.

Progress: consider a remote service that takes 5 seconds to respond. The download number will be stale until the derived store calls set. Making the value change to a Promise for the duration would be more fitting for many uses.

Errors: What if the server responds 500 internal server error? The example keeps showing the old number.

You can construct both by setting _other_ properties inside the Promise callbacks, but we have {#await} and I feel like it should be used for this, instead of special casing the logic every time.

Me personally, I want to submit a form field to the server when it's changed (with debounce & idle), and notify user with messages like "Saving..." and display errors. If I could easily derive a "status" prop from the form field value, I'd be happy. It seems possible but it's only simple if you neglect aspects of it.

@tv42 In my experience, the type of async reactive declarations requested in this issue explicitely asks for keeping stale data until new data is available, mostly to avoid unwanted flashes of content until that new data is available.

If you don't need the stale data and just want to use promises for progress tracking, your code would be as simple as this (and even safe from race-conditions!):

let package_name = 'svelte';
$: download_count = fetch('https://api.npmjs.org/downloads/point/last-week/' + package_name)
    .then(response => response.json())
    .then(data => download_count = data.downloads || 0 })

That said, while your needs for progress & error handling are very valid, to me they don't sound like something a runtime-frowning framework like Svelte should handle.

Your points could also be funneled into my previous comment's examples relatively easily (additional boolean helper variable/store for progress tracking and direct error propagation for failure):

With reactive statements:

let package_name = 'svelte';
let downloading = false;
let download_count = 0;
$: {
    downloading = true;
    fetch('https://api.npmjs.org/downloads/point/last-week/' + package_name)
        .then(response => response.json())
        .then(data => download_count = data.downloads || 0 })
        .catch(error => download_count = error) // check for error in template
        .then(() => downloading = false);
};

A little more code shifting is required with derived stores:

import { writable, derived } from 'svelte/store';

const package_name = writable('svelte');
const downloading = writable(false);
//    ^-- could also use a `readable` store to prevent manipulation
//       from the outside, but let's keep it writable for brevity
const download_count = derived(
    package_name,
    ($package_name, set) => {
        // Flag to keep track of possible newer derivations
        let is_latest = true;

        fetch('https://api.npmjs.org/downloads/point/last-week/' + $package_name)
            .then(response => response.json())
            .then(data => is_latest && set(data.downloads))
            .catch(error => is_latest && set(error)) // check for error in template
            .then(data => is_latest && downloading.set(false));

        // Mark this context as superseded on cleanup
        return () => is_latest = false
    }
);

I ended up writing a custom store that "buffers" sets for both a small time interval and ensuring only one async action is in flight (and triggering an extra round of async processing if a set was seen after the last async action was launched). Usage example:

let foo = 42
async function submit(value) {
    ...
}
const status = delayed(submit, 1000)
$: $status = foo

Reading $status gives a Promise that resolves to the return value of latest submit call, or throws.

Update: ... which sort of sucks because the $: makes submit trigger once at start, unwanted. Right now I'm working around that with if (value == foo) { return } inside submit.

What I'm wondering is why it doesn't work to do something like this:

<script>
  import { getItems } from './getItems'

  $: filtered = getItems($filter)
</script>

  {#await filtered}
    <Grid>
      {#each Array(12).fill(0) as i}
        <Skeleton />
      {/each}
    </Grid>
  {:then data}
    {#if data && data.length > 0}
      <Grid>
        {#each data as item}
          <Card {item} />
        {/each}
      </Grid>
    {:else}
        <h3>Nothing matched your search, try broadening your filters</h3>
    {/if}
  {/await}

Recently I wrote a component with the pattern above, where $filter is a store updated every time the user changes the filters (like category, genre, etc.), and getItems() is an async function that calls fetch with those params.

It seemed to work quite well, but then on further inspection I noticed that sometimes -- seemingly randomly -- the cards wouldn't update. Everything else was updating -- $filter, filtered -- and getTitles was being called. But it didn't seem to trigger the {#await} block properly.

Once I replaced it with the .then() syntax proposed by @loilo, everything worked as expected. But why doesn't the simpler syntax of $: filtered = getItems($filter) and then {#await} not work? Any explanation would be much appreciated! Thank you :)

Another way to do the same is:

import { getItems } from './getItems';

let filtered;
$: (async() => filtered = await getItems())();

So, I don't think we should have aditional processing of this.

@PaulMaly Fantastic, that's exactly what I was looking for! Thank you!

I think this can be closed. As noted above, you can already do this with existing syntax.

@PaulMaly Tried your solution:

  $: (async() => {
    rows = await sourceDefinition.fetch(sortBy, sortAsc);
    console.log('DATA', rows);
  });

This is never called. I need this statement to be called when sortBy and sortAsc are changed. Am I missing something?

Note: An example on the official documentation/examples would be a nice addition.

I ended up with this workaround:

  const update = async (sortBy, sortAsc) => {
    rows = await sourceDefinition.fetch(sortBy, sortAsc);
    console.log('DATA', rows);
  }
  $: update(sortBy, sortAsc);

Works great, but I have to declare an additional function to pass my arguments. I'm quite uncomfortable with this solution, can this be done with a better way? :thinking:

@soullivaneuh You just need to fix your code exactly as I wrote before. Pay attention, this is not a regular function, it's IIFE.

Also, you can find here even more examples of reactive expressions which you won't find in official docs/examples.

I finally found a simpler and more reliable solution:

<script>
  $: {
    rows = sourceDefinition.fetch(sortBy, sortAsc, search, query);
    console.log('Updated rows:', rows);
  };
</script>

{#await rows}
  Loading...
{:then resolvedRows}
  <DataTable {columns} rows={resolvedRows} />
{:catch error}
  <p class="text-red-500">{error.message}</p>
{/await}

Thanks for the feedback @PaulMaly!

@soullivaneuh Your solution is a very basic. The case above is more complex because using your solution you can't manipulate with fetched data outside of template and even outside {#await / } tag. So, if you need a read-only solution it's good but otherwise, it won't help you.

Was this page helpful?
0 / 5 - 0 ratings