Svesta Docs

svesta - is a tiny svelte state management library for svelte/sveltekit.

Installation

# In your project directory
npm install svesta

or

# In your project directory
yarn add svesta

Usage

Once you’ve added svesta to your project, use it as shown below, in your project:

+page.svelte (without page.ts for data)

<script>
	import { onMount } from 'svelte';
	import { type StoreMeta, type StoreProps, type StoreState, Transport, useStore, Resource } from 'svesta';

	import { resultTransformer } from './sample/transformer.js';
	import Users from './sample/users.svelte';
	import Item from './sample/item.svelte';
	import type { User } from './sample/types.js';

	let meta: StoreMeta;

    const usersProps:StoreProps<User> = {
		resultTransformer
	};
	const users = useStore<User>('users', usersProps);

	const handleNext = () => users.pageTo(meta.page + 1);

	const handlePrev = () => users.pageTo(meta.page - 1);

	const onMore = () => users.next();
	onMount(() => {
		// Ensure BASE_URL is already set on Transport
		// It is better done one time inside +layout.ts
		// instead of inside every view like the following
		Transport.configure({ BASE_URL: 'https://reqres.in/api'});
		users.sync();
	})
</script>

+page.svelte (using page.ts for data)

<script>
    // from sveltekit
	import type { PageData } from './$types.js';

	import { type StoreMeta, type StoreProps, type StoreState, useStore, Resource } from 'svesta';

	import { resultTransformer } from './sample/transformer.js';
	import Users from './sample/users.svelte';
	import Item from './sample/item.svelte';
	import type { User } from './sample/types.js';

	export let data: PageData;

	let meta: StoreMeta;

    const usersProps:StoreProps<User> = {
		initData: data as StoreState<User>,
		resultTransformer
	};
	const users = useStore<User>('users', usersProps);

	const handleNext = () => users.pageTo(meta.page + 1);

	const handlePrev = () => users.pageTo(meta.page - 1);

	const onMore = () => users.next();
</script>

+page.ts

// from sveltekit
import type { PageLoad } from './$types.js';

import { Transport } from 'svesta';
import { resultTransformer } from './sample/transformer.js';

export const load: PageLoad = async ({ fetch }) => {
	// This is happening on the server and we have a ref to a fetch
	// implementation, let's use it by passing it to the configure
	// method of Transport
	Transport.configure({ BASE_URL: 'https://reqres.in/api', fetch });
	const { error, ...rest } = await Transport.get('/users');
	return { ...resultTransformer(rest), error };
};
<h1 class="header"><strong>Svesta</strong> Docs</h1>

<Resource store="{users}" let:items bind:meta busy="blocked">
	<Users users="{items}" let:user>
		<Item {...user} />
	</Users>
	{#if meta}
	<div class="buttons">
		<button on:click="{handlePrev}" disabled="{meta.page === 1}">Previous page</button>
		<button on:click="{handleNext}" disabled="{meta.page === meta.pages}">Next page</button>
		<button on:click="{onMore}" disabled="{meta.page === meta.pages}">
			more(append to view)...
		</button>
	</div>
	{/if}
</Resource>

transformer.js

import type { Params } from 'svesta';

export const resultTransformer = (raw: Params = {}) => {
	const { page, per_page: limit, total: recordCount, total_pages: pages, data } = raw;
	return { page, limit, recordCount, pages, data };
};

types.ts

export type User = {
	id: number;
	email: string;
	first_name: string;
	last_name: string;
	avatar: string;
};

users.svelte

<script>
	import type { User } from './types.js';
	export let users: User[];
</script>
<ul class="directories">
	{#if users} {#each users as user, i}
	<slot {user} />
	{/each} {/if}
</ul>

item.svelte

<script>
	export let avatar: string = '';
	export let first_name: string = '';
	export let last_name: string = '';
	export let email: string = '';
</script>
<li class="directory">
	<div class="image">
		<img src="{avatar}" alt="user avatar" loading="lazy" />
	</div>
	<div class="detail">
		<strong>{first_name} {last_name}</strong>
		<em>{email}</em>
	</div>
</li>

There are 5 major exported components in svesta, they are:

Let us examine each component in detail.

1. Transport

Transport is useful in requesting any arbitrary HTTP(S) endpoint. Transport exposes a configure method that allows the customization of HTTP(S) requests. Such customization could be achieved as below:

import { type TransportConfig, Transport } from 'svesta';
const configOptions:TransportConfig = {
    // Log out details or not. Default is false.
    DEBUG: true;
    // Set the base URL. Default is ''.
    BASE_URL: 'https://some-base-url';
    // External fetch implementation could be passed.
    // Default is undefined.
    // Either window.fetch or fetch from
    // +page.[t|j]s' onload as shown in our example.
    fetch: window.fetch;
    // Allow or disallow migration to web socket.
    // Default is false.
    realTime: false;
    // Allows the override of the base fetch RequestInit.
    // Default is undefined.
    init: RequestInit;

};
Transport.configure(configOptions);

Transport competently handles GET, POST, PUT, PATCH, OPTIONS and DELETE HTTP methods via the exposed methods: .get, .post, .put, .patch, .delete, .options and .upload. An example could look like the following:

const { error, data } = await Trasport.post('/users', { name: 'Name', address: 'Some address' });

Transport also exposes an isOnline field indicative of whether network connection is available or not. This field changes with every network changes.

2. useStore

If the target of requests are RESTful APIs, then the appropriate component from svesta is the useStore. useStore defines 2 parameters:

Note: useStore uses Transport under the hood, therefore, configurations set on Transport also affect useStore.

import { type StoreProps, useStore } from 'svesta';

// Create a store props
const usersProps: StoreProps<User> = {
	// Parameters. Default {}
	params: {},
	// Query order asc, desc or [asc|desc]|<field>
	// e.g "desc|id". Default is 'asc'
	orderAndBy: 'asc',
	// namespace allows you proxy a resource with another name
	// E.g. 'profile' could still be used as 'users' namespace.
	// That is profile is not known to you system but 'users'
	// namespace defaults to ''.
	namespace: '',
	// You can pre-populate a store by passing `initData`.
	// It defaults to {}
	initData: {},
	// includes helps control what fields are returned
	// It is pipe-separated string of resource names.
	// resource name is comma-saparate string of fields
	// E.g. 'users:name, age, gender | accounts: accountId, balance' where
	// users and accounts are resource names and their fields are listed after `:`
	// The above example is for when there are relationship
	// in multiple resources(join).
	// It defaults to ''.
	includes: '',
	// resultTransformer is a function you can pass
	// to intercept and convert the data from your API to what
	// `useStore` understands, the StoreState type.
	// This is important when using the `.sync` method of store
	// to synchronize/fetch data from your APIs.
	// It accepts the raw data from your API and
	// returns the transformed data. See transformer.js above.
	// It defaults to undefined
	resultTransformer: undefined,
	// queryTransformer is a function you can pass
	// to intercept and convert your query to what your API expects.
	// This is important when using the `.sync` method of store
	// to synchronize/fetch data from your APIs.
	// It accepts the raw query and
	// returns the transformed query for your API.
	// It defaults to undefined
	queryTransformer: undefined
};
// Let's create a users store
const users = useStore(
	// resource name, not path please. Required
	'users',
	// store props. Optional
	usersProps
);

// Of all the store arguments, the `resource name` is mandatory
// StoreProps is optional and could be omitted, for instance:
const people = useStore('users');

useStore maintains its internally structure as a StoreState<T>. In our example above, T is User. The structure as as shown below:

type StoreState<T> = {
	// result target
	data: T[];
	// total record count
	recordCount: number;
	// Number of pages for limit per page
	pages?: number;
	// What page number are we on?
	page?: number;
	// Record per page
	limit?: number;
	// Are we processing network?
	loading?: boolean;
	// Are there error based on request
	error?: string | null | undefined;
	// Params that produced result or error
	params?: Params;
};

The reason for a resultTransformer, is to convert API responses that do not conform to StoreState<T>. Look at the API in our example, that is, regres.in. regres.in API returns its response as:

type IngressType = {
	page: number;
	per_page: number;
	total: number;
	total_pages: numbers;
	data: unknow;
};

export const resultTransformer = (raw: Params = {}) => {
	const { page, per_page: limit, total: recordCount, total_pages: pages, data } = raw;
	return { page, limit, recordCount, pages, data };
};

However, useStore cannot store data like this, hence, the need to implement a result transformer to convert as below:

export const resultTransformer = (raw: Params = {}) => {
	// raw is what comes from regres.in
	const { page, per_page: limit, total: recordCount, total_pages: pages, data } = raw;
	// return what conforms to StoreState
	return { page, limit, recordCount, pages, data };
};

useStore exposes the following methods:

import { useStore } from 'svesta';
import type Account from '...';

const accounts = useStore<Account>('accounts',...);
// force sync
accounts.sync();
// or with initial data
// initData is of type StoreState<Account>
accounts.sync(initData)
import { useStore, Resource, type StoreMeta } from 'svesta';
import type Account from '...';

let meta: StoreMeta;
const accounts = useStore<Account>('accounts',...);
const onNext = () => accounts.pageTo(meta.page + 1);
const onPrev = () => accounts.pageTo(meta.page - 1);
const onMore = () => accounts.next();
<Resource store="{accounts}" let:items bind:meta busy="blocked">
	{#each items as account}
	<Item {...account} />
	{/each} {#if meta}
	<div class="buttons">
		<button on:click="{onPrev}" disabled="{meta.page === 1}">Previous page</button>
		<button on:click="{onNext}" disabled="{meta.page === meta.pages}">Next page</button>
		<button on:click="{onMore}" disabled="{meta.page === meta.pages}">
			more(append to view)...
		</button>
	</div>
	{/if}
</Resource>
import { useStore } from 'svesta';
import type Account from '...';

const accounts = useStore<Account>('accounts',...);
// Fire and forget
accounts.save({accountName:'Account Name', accountNo:1234});

// Fire and inspect
const { error, message, data, status } = await accounts.save({accountName:'Account Name', accountNo:1234});
// Do something with fields
import { useStore } from 'svesta';
import type Account from '...';

const accounts = useStore<Account>('accounts',...);
// Fire and forget
accounts.destroy({id:12345678});

// Fire and inspect
const { error, message, data, status } = await accounts.destroy({id:12345678});
// Do something with fields
import { useStore } from 'svesta';
import type Account from '...';

const accounts = useStore<Account>('accounts',...);
// Fire and forget
// Seach can be added and customized via `queryTransformer`
accounts.search('google');
import { onMount } from 'svelte';
import { useStore } from 'svesta';
import type Account from '...';

const accounts = useStore<Account>('accounts',...);
onMount(async () =>{
    // The following will make a GET request to `/accounts/checking`
    // path using the passed params
    const { data, error} = await accounts.get('/checking',{...});
})
import { onMount } from 'svelte';
import { useStore } from 'svesta';
import type Account from '...';

const accounts = useStore<Account>('accounts',...);
onMount(async () =>{
    // The following will make a request to POST `/accounts/checking`
    // path using the passed params
    const { data, error} = await accounts.post('/checking',{...});
})
import { useStore } from 'svesta';
import type Account from '...';

const accounts = useStore<Account>('accounts',...);
// Seach for `12345` on the `accountId` field
const targetAccount = await accounts.find(key:'accountId', value:12345);
// Check and use targetAccount
import { useStore } from 'svesta';
import type Account from '...';

const accounts = useStore<Account>('accounts',...);
// Filters for  `accountName` that equals 'Steve Samson'
 accounts.filter({accountName:'Steve Samson'});
import { type EventHandler, useStore } from 'svesta';
import type Account from '...';

const accounts = useStore<Account>('accounts',...);
const onAccountUpdate:EventHandler = (account:Account) =>{
    console.log(`${account} was updated!`);
};

// onAccountUpdate will be called when update
// happens on any account in the store
accounts.on('update',onAccountUpdate);
import { type EventHandler, useStore } from 'svesta';
import type Account from '...';

const accounts = useStore<Account>('accounts',...);
// id is required
const account = {accountName:'Account Name', accountNo:2345566, id:new Date().getTime()};
accounts.add(account);
accounts.patch({...account, accountName:'Updated Account Name'});
accounts.remove(account);

3. Resource

Like mentioned earlier, Resource, a svelte component, which helps manage the network requests while using useStore to manage REST resources. Resource also handles resolution of promises and notify of loading during an on-going network request, errors on events of HTTP errors or data availability on success.

Resource accepts 4 props:

const loaderComponent: SvelteComponentAsProp = {
	component: LoaderWithText,
	props: { text: 'Loading...' }
};

component is of type svelte ComponentType while props is any object describing all the props of the component. Resource comes with a default LoaderComponent that is used when none is provided.

import { type StoreMeta } from 'svesta';

export let meta: StoreMeta = { page: 1, pages: 1 };

Besides these props, Resource exposes the following slot props:

Resource is just a svelte component used as below:

// svelte file
<script>
import { type StoreMeta, useStore } from 'svesta';
import type Account from '...';

let meta: StoreMeta;
const accounts = useStore<Account>('some');

</script>
<Resource store="{accounts}" let:items let:loading let:error let:meta bind:meta busy="blocked">
	<!-- make use of  -->
</Resource>

TODO

At the moment, Transport.configure sets an app-wide settings for svesta, I love to implement a none-global configuration, a per-need type.