Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async reactive declarations #2118

Closed
Tracked by #1
ansarizafar opened this issue Feb 21, 2019 · 18 comments
Closed
Tracked by #1

Async reactive declarations #2118

ansarizafar opened this issue Feb 21, 2019 · 18 comments
Labels
awaiting submitter needs a reproduction, or clarification

Comments

@ansarizafar
Copy link

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.

@Conduitry
Copy link
Member

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.

@Conduitry Conduitry added the awaiting submitter needs a reproduction, or clarification label Apr 14, 2019
@loilo
Copy link

loilo commented May 15, 2019

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.

@tv42
Copy link

tv42 commented Aug 12, 2019

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

@loilo
Copy link

loilo commented Aug 12, 2019

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

@tv42
Copy link

tv42 commented Aug 13, 2019

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.

@jhwheeler
Copy link

jhwheeler commented Sep 15, 2019

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

@PaulMaly
Copy link
Contributor

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.

@jhwheeler
Copy link

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

@Conduitry
Copy link
Member

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

@soullivaneuh
Copy link

soullivaneuh commented Feb 9, 2020

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

@soullivaneuh
Copy link

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? 🤔

@PaulMaly
Copy link
Contributor

PaulMaly commented Feb 9, 2020

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

@soullivaneuh
Copy link

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!

@PaulMaly
Copy link
Contributor

PaulMaly commented Feb 9, 2020

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

@trev-dev
Copy link

trev-dev commented Aug 1, 2021

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.

Apologies for the necrobump, but out of curiosity, is this async (which might be an ajax call to somewhere) be fired every time some other piece of state updates?

@PaulMaly
Copy link
Contributor

PaulMaly commented Aug 1, 2021

@trev-dev yep, every time when its dependencies will changed.

@boukeversteegh
Copy link

boukeversteegh commented Jan 26, 2022

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.

It looks more declarative than then(r => filtered = r), which is a benefit, but the syntax is quite hard to read and write, especially for non hard-core js developers.

Wouldn't it be great if this pattern could be simply written as:

let filtered = await getItems();

Basically, this statement is currently not allowed as await is not within an async function, but svelte could allow it and just recompile to the above. Or is this too crazy? Here as a svelte new-comer.

@alexmt
Copy link

alexmt commented May 5, 2022

Here is another approach in case you need to load data asynchronously and then apply e.g. client-side filtering:

{#await rows}
  Loading...
{:then resolvedRows}
  {@const filteredRows = clusters.filter((item) => matches(item, filter))} // apply client side filtering once and store in `filteredRows` const
  <DataTable {columns} rows={filteredRows} />
{:catch error}
  <p class="text-red-500">{error.message}</p>
{/await}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting submitter needs a reproduction, or clarification
Projects
None yet
Development

No branches or pull requests

10 participants