1
\$\begingroup\$

In mySvelte app, I need to reactively fetch data from the server and then render the result. The ideal data struture for this is astore whose values are promises – eg. like this:

let article = writable("about");let text = derived(article, a => fetch(`http://example.com?article=${a}`));

However, the way I want to render the values is different from this representation: if thearticle variable changes, I still want to render the previous text and overlay it with a spinner until the new text loads (ie. until the promise resolves). Because something like this could be useful in more places, I decided to write a little helper function for this.

TheawaitedStore function takes a store whose values are promises, and returns two new stores:lastValue andsettled. ThelastValue store contains the last resolved value – if the original store changes to a new promise,lastValue won't update until the promise is resolved. Theresolved store contains a boolean indicating whether the current promise is settled or not – the UI can display a spinner any timeresolved is false.

There are a few other important features:

  • if neitherlastValue norresolved have a subscriber, then we don't subscribe to the original store
  • never updatelastValue with an outdated promise – if a new promise comes before the old was resolved, we stop waiting until it resolves

This is the code:

import { writable, type Readable } from "svelte/store"function cancellableThen<T>(p: Promise<T>, f: (v: T) => void): () => void {  let canceled = false;  p.then((v) => {    if (canceled) return;    f(v);  });  return () => (canceled = true);}const Void: void = void 0;export function awaitedStore<T>(store: Readable<Promise<T>>): {  lastValue: Readable<T | undefined>;  resolved: Readable<boolean>;} {  let cold = true;  let lastValueHasSubscriber = false;  let loadingHasSubscriber = false;  const start = (what: "lastValue" | "loading") => () => {    if (what === "lastValue") lastValueHasSubscriber = true;    if (what === "loading") loadingHasSubscriber = true;    const stop = () => {      if (what === "lastValue") lastValueHasSubscriber = false;      if (what === "loading") loadingHasSubscriber = false;      if (lastValueHasSubscriber || loadingHasSubscriber) return;      cold = true;      unsubPromise?.();      unsubStore();    };    if (!cold) return stop;    cold = false;    let unsubPromise = () => Void;    const unsubStore = store.subscribe((p) => {      unsubPromise?.();      resolved.set(false);      unsubPromise = cancellableThen(p, (v) => {        lastValue.set(v);        resolved.set(true);      });    });    return stop;  };  const lastValue = writable<T | undefined>(undefined, start("lastValue"));  const resolved = writable(false, start("loading"));  return { lastValue, resolved };}

Link to TS playground.Link to Svelte REPL.

An obvious shortcoming of the code is the lack of error handling – I decided to omit it in favor of simplicity, and I intend to add it later.

Sᴀᴍ Onᴇᴌᴀ's user avatar
Sᴀᴍ Onᴇᴌᴀ
29.6k16 gold badges46 silver badges203 bronze badges
askedOct 19, 2022 at 20:41
a pfp with melon's user avatar
\$\endgroup\$
3
  • \$\begingroup\$For starters, how about using anAbortController for the cancellablePromise?\$\endgroup\$CommentedOct 21, 2022 at 22:22
  • \$\begingroup\$AbortController is cool but it's only supported by fetch. There are many other Promise usecases that don't involve fetch. Furthermore, AbortController would lead to unwanted behavior unlessawaitedStore is the sole consumer of those promises. Eg. if there were two components that use the same fetched data in different ways, one of them could cancel a request that is awaited by the other one. My code makes no such assumptions. To sum up: while I agree that, in this specific usecase, AbortController would make the code more optimized, it would also be less general and would introduce a footgun\$\endgroup\$CommentedOct 24, 2022 at 23:16
  • \$\begingroup\$Can you explain why you need this part: if neitherlastValue norresolved have a subscriber, then we don't subscribe to the original store. I don't understand why you need this\$\endgroup\$CommentedNov 1, 2022 at 19:31

1 Answer1

2
\$\begingroup\$

YourawaitedStore function is rather verbose. All of the functionality for unsubscribing and resubscribing to the input store seems unnecessary. I also noticed that since thecancellableThen function is only used once and never exported, it made sense just to inline the functionality.

Here is a shorter implementation ofawaitedStore:

// awaited-store.tsimport { writable, type Readable } from "svelte/store"export function awaited<T>(store: Readable<Promise<T>>): {  loading: Readable<boolean>;  value: Readable<T>;} {  const value = writable<T>();  const loading = writable<boolean>(true);  store.subscribe((promise) => {    loading.set(true);    const valueThen = get(value);    promise.then((result) => {      const valueNow = get(value);      if (valueThen !== valueNow) return;      value.set(result);      loading.set(false);    });  });  return { loading, value };}

And it is used the same way, I just renamedsettled toloading andlastValue tovalue because those names seemed more obvious to me as to what the variable represented.

<script lang="ts">  import { writable, derived } from "svelte/store";  import { awaited } from "./awaited-store.ts";  import { fetchPost } from "./utils.ts";  const postId = writable(1);  const article = derived(postId, (id) => fetchPost(id));  const { loading, value } = awaited(article);</script>{#if $loading}  <p>Loading...</p>{/if}{#if $value}  <article>    <h2>{$value.title}</h2>    <p>{$value.body}</p>  </article>{/if}

Demo link on Stackblitz

Let me know if there are any issues; I haven't tested this thoroughly yet.

answeredNov 1, 2022 at 19:40
Jacob's user avatar
\$\endgroup\$
0

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.