sveltejs / svelte

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

Runes + async: add (or document) idiomatic way to work with async functions and runes #10982

Open ivan-lednev opened 3 months ago

ivan-lednev commented 3 months ago

Describe the problem

In my Svelte 4 app, I keep all the logic inside stores and away from components. I found this approach to be more predictable and bug-free.

Here is an example of a derived store that updates whenever the user changes the settings, that relies on derived(v, (v, set) => { ... }). I don't understand how to rewrite this using runes.

Using $effect to synchronize state this way seems like an anti-pattern, not to mention the fact that since my state logic is outside the component tree, and I'd have to manage cleanup myself with $effect.root.

I've searched through the new docs, and I haven't encountered the word async there 😢.

Describe the proposed solution

Solid.js has an createResource: https://docs.solidjs.com/reference/basic-reactivity/create-resource , which is exactly this: a signal that updates with an async function whenever its deps update.

Importance

would make my life easier

fuwaneko commented 2 months ago

I don't understand how to rewrite this using runes.

You don't have to. Runes are not a replacement for Svelte 4 stores, they work fine together. It would be nice to have async function support for derived stores, though.

brunnerh commented 2 months ago

Runes are a replacement, but stores are not being deprecated yet. See:

Runes are an additive feature, but they make a whole bunch of existing concepts obsolete: ...

  • the store API and $ store prefix (while stores are no longer necessary, they are not being deprecated)

Using $effect to synchronize state this way seems like an anti-pattern, not to mention the fact that since my state logic is outside the component tree, and I'd have to manage cleanup myself with $effect.root.

For asynchronous things, using $effect is, as far as I know, currently the expected way to do it.

If you have state outside the component lifecycle, that sounds more like an issue to me since it points towards global state which is rarely a good idea and an outright hazard if you are using server-side rendering.

Simple example:

export function getAsync(init, compute) {
  let val = $state(init);

  $effect(() => {
    compute().then(result => {
      if (result != getAsync.valueChanged)
        val = result;
    });
  })

  return () => val;
}

getAsync.valueChanged = Symbol('value changed');
let value = $state(1);
let derivedValue = $derived.by(getAsync(
  undefined,
  async () => {
    // dependencies need to be read before first `await` happens
    const currentValue = value;
    const result = await fetchData(currentValue);
    if (currentValue !== value)
      return getAsync.valueChanged;

    return result;
  },
));

REPL

The reading of the dependencies before async code could be further enforced by having an API that splits the logic into reading and calculating. E.g. with a signature like this:

function getAsync(
  init: T,
  dependencies: () => D,
  compute: (dependencies: D) => Promise<T>,
): () => T;
fuwaneko commented 2 months ago

@brunnerh yes, I phrased my comment vaguely, but I meant that stores are not deprecated yet.

Furthermore, I noticed it's possible to return a Promise from $derived.by or pass an async function to it, thus avoiding $effect.

E.g.

async function asyncFetch(search: string) {
  // do some fetching
}

let value = $state('') // will be used as an initial value
let derivedValue = $derived.by(() => {
  return asyncFetch(value)
})
// or
let derivedValue = $derived.by(async () => {
  const data = await asyncFetch(value)
  // do some transformations on data
  return transform(data)
})

{#await derivedValue}
  <div>Loading...</div>
{:then value}
  {value}
{/await}

This is similar to what is achieved with createResource in SolidJS.