UNPKG

svelte-asyncable

Version:

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

225 lines (161 loc) 7.17 kB
# Super tiny, declarative, optimistic, async store for SvelteJS. [![NPM version](https://img.shields.io/npm/v/svelte-asyncable.svg?style=flat)](https://www.npmjs.com/package/svelte-asyncable) [![NPM downloads](https://img.shields.io/npm/dm/svelte-asyncable.svg?style=flat)](https://www.npmjs.com/package/svelte-asyncable) ## Features - Extends the [Svelete store contract](https://svelte.dev/docs#4_Prefix_stores_with_$_to_access_their_values) 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 ```bash npm i svelte-asyncable --save ``` ```bash yarn add svelte-asyncable ``` CDN: [UNPKG](https://unpkg.com/svelte-asyncable/) | [jsDelivr](https://cdn.jsdelivr.net/npm/svelte-asyncable/) (available as `window.Asyncable`) If you are **not** using ES6, instead of importing add ```html <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. ```javascript 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: ```javascript 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: ```javascript // 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: ```javascript 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: ```javascript 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`. ```javascript 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. ```javascript 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. ```javascript 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: ```javascript 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. ```svelte {#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. ```javascript 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: ```javascript 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. ```javascript export const pinsStore = asyncable(async () => { const $pinstStore = await pinsStore.get(); if ($pinstStore.length > 0) return $pinstStore; return getAllPins(); }); ``` ## License MIT &copy; [PaulMaly](https://github.com/PaulMaly)