0.0.16 • Published 6 months ago

@betomorrow/micro-stores v0.0.16

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

Micro-stores

A light state management library featuring observables and immutability

Usage

import { Store } from "micro-stores";

const bookStore = new Store((id) => fetch(`https://myApi.com/books/${id}`));
const myFavoriteBook = bookStore.getObservable("dracula");

console.log(myFavoriteBook.get()); // null
bookStore.fetch("dracula");
console.log(myFavoriteBook.get()); // { id: "Dracula", ... }

⚠️ Usage with React Native ⚠️

This library depends on uuid. To use uuid, and therefore micro-stores with React Native, you need to follow the steps described at https://github.com/uuidjs/uuid#react-native--expo:

  • Install react-native-get-random-values
  • Run a pod install
  • Import it at the root of your app:
    import 'react-native-get-random-values';

Micro-observables

This library is quite heavily based on micro-observables. You may want to take a look at the Observable signature there.

Api

Micro-stores exposes 3 main Stores that can be used to easily manage your application state.

Store

A simple Store retrieving items using a primary key.

import { Store } from "micro-stores";

const bookStore = new Store((id) => fetchBook(id));
bookStore.fetch();

To create a Store, you have to provide a fetcher function retrieving objects using an unique identifier. By default, your store will use the id property of the object (if it exists).
If your object doesn't have an id property, you will need to use one, or specify another unique property to be used.

Constructor parameters

ParameterTypeDefault ValueDescription
fetch(key: string) => T | Promise\<T>/The function retrieving item by its key
primaryKeystring"id"The primary key to use to map your objects

Builder Methods

bindProperty

You can enrich your Store by binding a property to another Store using the bindProperty method.
This is useful if you want to ensure yours objects are up to date when making changes to its referenced property. The Store will use the referenced property unless it is removed from its own Store

Usage

import { Store } from "micro-stores";

const userStore = new Store(fetchUser);
const bookStore = new Store(fetchBook).bindProperty("infos.author", userStore);

bookStore.fetch("dracula");
console.log(userStore.getObservable("bram-staker").get());
// { id: bram-stoker, name: "Bram Stoker" }
userStore.save({ id: "bram-staker", name: "Bram" });
console.log(bookStore.getObservable("dracula").get().infos.author);
// { id: "bram-staker", name: "Bram" }
userStore.remove("bram-staker");
console.log(bookStore.getObservable("dracula").get().infos.author);
// { id: "bram-staker", name: "Bram Staker" }

presentProperty

It has the same purpose as bindProperty, but can be used with lighter objects. This means that when fetching items from your store, it will not populate the presented Store with light values. However, it will update the presented store with updated value if they already exist.

MethodTypeDescription
bindProperty(path: string, referenceStore: Store\<U>) => Store\<T>Binds your item property to another Store
presentProperty(path: string, referenceStore: Store\<Partial\<U>>) => Store\<T>Presents your item property to another Store

Methods and properties

Main methods and properties:

PropertyTypeDescription
primaryKeystringThe primary key to use to map your objects
itemsObservable\<Map\<string, T>>The observable of the items mapped by their key
getObservable(key: string) => Observable\<T>Retrieve an observable using its key
fetch(key: string) => PromiseCall the Store fetch function and saves the received item
save(item: T) => voidSave an item to the Store. If an items exists will the same key, it will be erased
merge(items: T[]) => voidSave several items at once
remove(key: string) => voidRemove an item from the Store
update(key: string, updater: (current: T) => T) => voidUpdate an item using an update callback, if it exists
updateProperties(item: Partial) => voidUpdate an items with specified properties, if it exists
batchUpdateProperties(items: Partial[]) => voidUpdate several items with specific properties, if they exists
clear() => voidClears the store
onDeleteSignalCalled when an item is removed from the Store

PaginatedStore

A PaginatedStore stores items in an Array and handles pagination for you using Pages.

import { PaginatedStore } from "micro-stores";

const bookStore = new PaginatedStore((page) => fetchBooks(page));
bookStore.list();
bookStore.listMore();
bookStore.listMore();

To create a PaginatedStore, you have to provide a fetcher function retrieving a page of objects using an page number.

A Page is an interface defined by this properties:

interface Page {
	content: T[];
	page: number;
	totalPages: number;
	totalSize: number;
}

Constructor parameters

ParameterTypeDescription
fetchList(page: number) => Promise<Page> | Page)The function retrieving Page by its index

Builder Methods

bind

You can bind your PaginatedStore to another Store using the bind method.
This will allow you to show in your list of items the actual items from the binded Store, thus ensuring them to be up to date. The binded Store will also be automatically updated with the values retrieved when listing objects from the PaginatedStore You can only bind a PaginatedStore to a Store that stores the exact same interface of objects. Meaning that your PaginatedStore will have to use the same unique identifier property as your simple Store.
You can only bind your PaginatedStore to a single Store.

Usage

import { Store, PaginatedStore } from "micro-stores";

const bookStore = new Store(fetchBook);
const favoriteBookStore = new PaginatedStore(fetchBook).bind(bookStore);

favoriteBookStore.list();
console.log(bookStore.getObservable("dracula").get());
// { id: "dracula", name: "Dracula" }
bookStore.save({ id: "dracula", name: "Dracula 2" });
console.log(favoriteBookStore.paginatedItems.get().content[0]);
// { id: "dracula", name: "Dracula 2" }
bookStore.remove("dracula");
console.log(favoriteBookStore.paginatedItems.get().content[0]);
// null

present

present is very similar to the bind building method. The difference being it allows you to present from a Store items that are partials objects stored in a PaginatedStore.
For performance purpose, prefer using bind over present if your Store and your PaginatedStore use the exact same objects.

Your can only bind or present one single Store

MethodTypeDescription
bind(referenceStore: Store\<T>) => Store\<T>Binds your Paginated Store to another Store
present(referenceStore: Store\<U extends T>) => Store\<T>Binds your Paginated Store to another Store

Methods and properties

Main methods and properties:

PropertyTypeDescription
fetchingObservable\<boolean>Is the store fetching initial items ?
fetchingMoreObservable\<boolean>Is the store fetching more items ?
paginatedItemsObservable\<Page\<T> | null>The observable page of the items
list() => voidCall the Store fetchList function for the first page and erases the existing items
listMore() => voidCall the Store fetchList function and merge the new items

MappedStore

A MappedStore stores paginated arrays of items in an Map.
It is quite similar to PaginatedStore, also allowing you to store your paginated items according to specified keys.

import { MappedStore } from "micro-stores";

const bookStore = new MappedStore((userId, page) => fetchFavoriteBooksForUser(userId, page));
bookStore.list("user-1");
bookStore.listMore("user-1");
bookStore.list("user-2");

To create a MappedStore, you have to provide a fetcher function retrieving a page of objects using a mapping key and page number.

Constructor parameters

ParameterTypeDescription
fetchList(id: string, page: number) => Promise<Page> | Page)The function retrieving Page by its index

Builder Methods

bind and present

Just like a PaginatedStore, a MappedStore allows you to bind/present another Store.

Methods and properties

Main methods and properties:

PropertyTypeDescription
getFetching(key:string) => Observable\<boolean>Is the store fetching initial items for this key?
getFetchingMore(key:string) => Observable\<boolean>Is the store fetching more items for this key?
getObservableItems(key:string) => Observable\<Page\<T> | null>The observable page of the items
list(key: string) => voidCall the Store fetchList function for this key for the first page and erases the existing items
listMore(key: string) => voidCall the Store fetchList function for this key and merge the new items
clear() => voidClears the store

Usage with React

This library makes State Management easier for any nodeJS or browser application, and has been especially thought to be used with React.
This is why Micro-stores also gives you hooks to help you manage and retrieve the state of your React project:

useStore(key, store, fetchStrategy?, additionalDeps?)

Return the value of the matching the given key, the loading state and the current error. Triggers a re-render when the value changes.

import { Store, useStore } from "micro-stores";

const bookStore = new Store(fetchBook);

const DraculaBookView = () => {
	const { result: book, loading, error } = useStore("dracula", bookStore);

	if (book) {
		return (
			<div>
				{book.title} from {book.author}
			</div>
		);
	}
	if (loading) {
		return <div>Loading...</div>;
	}
	return null;
};

usePaginatedStore(paginatedStore, fetchStrategy?, additionalDeps?)

Returns a PaginatedDataResult of the given paginated store. Triggers a rerender when these properties change.

import { PaginatedStore, usePaginatedStore } from "micro-stores";

const bookStore = new PaginatedStore(fetchBooks);

const BookView = () => {
	const { result: books, listMore, lastPage, loading, moreLoading } = usePaginatedStore(bookStore);

	if (loading) {
		return <div>Loading...</div>;
	}
	return <div>
		<h2>Books</h2>
		{books.map(book => <BookView book={book}/>}
		{moreLoading && <div>Loading...</div>}
		{!lastPage && <button onClick={() => listMore()}>Load More</button>}
	</div>
};

useMappedStore(key, mappedStore, fetchStrategy?, additionalDeps?)

Similar to usePaginatedStore, only difference being you need to pass in the key you want to fetch.

PaginatedDataResult

The PaginatedDataResult is defined like this:

PropertyTypeDescription
resultT[]The current array of results
loadingbooleanIs the first page being loaded
moreLoadingbooleanAre more items beeing loaded
errorError |nullFetching error
lastPagebooleanAre all the pages fetched
totalPages?number | undefinedThe number of pages
totalSize?number | undefinedThe total size of the elements
list() => voidFunction to fetch the first page
listMore() => voidFunction to fetch the next page
clear() => voidClears the store

Fetch Strategy

The hooks above allow you to define a Fetch Strategy to decide how often the data should be fetch:

  • FetchStrategy.Always (default): The fetch function is called every time the component mounts or a dependency changes
  • FetchStrategy.Never: The fetch function is never called from the hook, you want to handle it yourself
  • FetchStrategy.First: The fetch function is only called if there is still no result associated with the data (useful if you already fetched the result in a parent component)
  • FetchStrategy.Once: The fetch function is called every time the component mounts.

Typescript

This library is entirely written in Typescript, meaning you can benefit from its typings without installing other packages.

0.0.11

8 months ago

0.0.12

8 months ago

0.0.13

8 months ago

0.0.14

8 months ago

0.0.15

7 months ago

0.0.16

6 months ago

0.0.10

1 year ago

0.0.9

1 year ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.3

3 years ago

0.0.5

3 years ago

0.0.4

3 years ago

0.0.6

3 years ago

0.0.2

3 years ago

0.0.1

3 years ago