svelte-asyncable

Super tiny, declarative, optimistic, async store for SvelteJS.


Keywords
svelte, svelte store
License
MIT
Install
npm install svelte-asyncable@1.3.0

Documentation

Super tiny, declarative, optimistic, async store for SvelteJS.

NPM version NPM downloads

Features

  • Extends the Svelete store contract with support for asynchronous values.
  • Store contains a Promise of a value.
  • Lazy-initialization on-demand.
  • Transparent and declarative way to describing side-effects.
  • Lets you update the async data without manual resolving.
  • Can derive to the other store(s).
  • Immutable from the box.
  • Optimistic UI pattern included.
  • Result of asynchronous call cached for lifetime of last surviving subscriber.

Install

npm i svelte-asyncable --save
yarn add svelte-asyncable

CDN: UNPKG | jsDelivr (available as window.Asyncable)

If you are not using ES6, instead of importing add

<script src="/path/to/svelte-asyncable/index.js"></script>

just before closing body tag.

Usage

Store with async side-effect that works as a getter.

Create your async store with the asyncable constructor. It takes a callback or getter that allows you to establish an initial value of the store.

import { asyncable } from 'svelte-asyncable';

const user = asyncable(async () => {
  const res = await fetch('/user/me');
  return res.json();
});

Please note, the result of this callback is not evaluated until it is first subscribed to or otherwise requested. (lazy approach).

There are two methods to access the stored promise, subscribe and get.

subscribe is a reactive subscription familiar from the Svelte store contract:

user.subscribe(async userStore => {
  console.log('user', await userStore); // will be printed after each side-effect
});

get returns a copy of the promise that will not update with the store:

// a point in time copy of the promise in the store

const userStore = await user.get();

Please note, the subscription callback will be triggered with the actual value only after a side-effect.

If the getter or callback provided to asyncable returns explicit undefined (or just return is omitted), the current value of the store won't updated. This may be useful, if we wish to only conditionally update the store. For example, when using svelte-pathfinder if $path or $query are updated, we may only wish to update the posts store on when in the posts route:

const posts = asyncable(($path, $query) => {
    if ($path.toString() === '/posts') {
      return fetch(`/posts?page=${$query.params.page || 1}`).then(res => res.json());
    }
  },
  null, 
  [ path, query ]
);

Store with async side-effect that works as a setter.

You can also pass async setter callback as a second argument. This function will be is triggered on each update/set operation but not after a getter call and receives the new and previous value of the store:

const user = asyncable(fetchUser, async ($newValue, $prevValue) => {
  await fetch('/user/me', {
    method: 'PUT',
    body: JSON.stringify($newValue)
  })
});

Every time the store is changed this setter or side-effect will be performed. The store may be modified selectively with update or completely overwritten with set.

user.update($user => {
  $user.visits++;
  return $user;
});

// or just set

user.set(user);

As the setter callback receives previous value, in addition to the new, you may compare current and previous values and make a more conscious side-effect. If setter fails the store will automatically rollback to the previous value.

const user = asyncable(fetchUser, async ($newValue, $prevValue) => {
  if ($newValue.email !== $prevValue.email) {
    throw new Error('Email cannot be modified.');
  }
  await saveUser($newValue);
});

Read-only asyncable store.

If you pass a falsy value (n.b. undefined excluded) as a second argument the asyncable store will be read-only.

const tags = asyncable(fetchTags, null);

tags.subscribe(async $tags => {
  console.log('tags changed', await $tags); // will never triggered
});

// changes won't actually be applied
tags.update($tags => {
  $tags.push('new tag');
  return $tags;
});

If you pass undefined as a second argument to asyncable, the store will be writable but without setter side-effect. The second parameter's default value is undefined so it is only required in the case you need to pass a third parameter. This will be useful in case bellow.

Dependency to another store(s).

Also, an asyncable store may depend on another store(s). Just pass an array of such stores as a third argument to asyncable. These values will be available to the getter. An asyncable may even depend on another asyncable store:

const userPosts = asyncable(async $user => {
  const user = await $user;
  return fetchPostsByUser(user.id);
}, undefined, [ user ]);

userPosts.subscribe(async posts => {
  console.log('user posts', await posts);
});

The getter will be triggered with the new values of related stores each time they change.

Using with Svelte auto-subscriptions.

{#await $user}
  <p>Loading user...</p>
{:then user}
  <b>{user.firstName}</b>
{:catch err}
  <mark>User failed.</mark>
{/await}

<script>
  import { user } from './store.js';
</script>

Simple synchronization with localStorage.

function localStore(key, defaultValue) {
  return asyncable(
    () => JSON.parse(localStorage.getItem(key) || defaultValue), 
    val => localStorage.setItem(key, JSON.stringify(val))
  );
}


const todos = localStore('todos', []);

function addTodoItem(todo) {
  todos.update($todos => {
    $todos.push(todo);
    return $todos;
  });
}

Get synchronous value from async store:

import { syncable } from 'svelte-asyncable';

const todosSync = syncable(todos, []);

Now you can use sync version of asyncable store in any places you don't need to have pending/fail states.

Caching:

The getter is only run once at first subscription to the store. Subsequent subscribe or get calls simply share this value while at least one subscription is active. If all subscriptions are destroyed, the getter is rerun on next subscription.

However, if the data on which your store depends changes infrequently, you may wish for a store to persist for the lifetime of the application. In order to achieve this you may conditionally return your initial value on the absence of an existing value.

export const pinsStore = asyncable(async () => {
    const $pinstStore = await pinsStore.get();
    if ($pinstStore.length > 0) return $pinstStore;
    return getAllPins();
});

License

MIT © PaulMaly