svelte-asyncable

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


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

Documentation

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

NPM version NPM downloads

Features

  • Stores value as the Promise.
  • 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.

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.

This callback 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();
});

Important, that getter won't be triggered before the first subscription (lazy approach).

If you need to have reactive subscription use subscribe method or just get current value (always the Promise) using get method:

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

// or once subscription

const $user = await user.get();

Important, subscription callback will be triggered with actual value only after side-effect completely performed.

If getter return undefined current value would keeped. It's useful, if we need to skip next value when one of dependencies is triggering, but actual store value shouldn't be changed. For example, integration with svelte-pathfinder:

const posts = asyncable(async ($path, $query) => {
    if ($path.toString() === '/posts') {
      const res = await fetch(`/posts?page=${$query.params.page || 1}`);
      return 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 triggered with new and previous value on each update/set operation but not after getter call:

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

Every time store has changed side-effect will be performed.

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

// or just set

user.set(user);

setter callback us also receives previous value to get the ability to compare current and previous values and make a more conscious side-effect. If setter failы store will automatically rollback value to the previous one.

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'll pass the obvious falsy value (except undefined) as a second argument it'll make asyncable store is 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'll pass undefined as a second argument store will be writable but without setter side-effect. It can 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. Related values will be passed as arguments of getter. For example dependence one asyncable store from another:

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

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

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.

License

MIT © PaulMaly