@known-as-bmf/store
Version:
Lightweight synchronous state management library.
263 lines (199 loc) • 8.31 kB
Markdown
# @known-as-bmf/store
Lightweight synchronous state management library.
<!-- [](https://travis-ci.org/known-as-bmf/store)
[](https://snyk.io/test/github/known-as-bmf/store?targetFile=package.json) -->
## Installation
`npm install --save @known-as-bmf/store`
## Description
This library is a reimplementation from scratch of [`@libre/atom`](https://github.com/libre-org/atom), that does not rely on global shared objects for internal plumbing and also add some API changes to make it more developer friendly.
It also keeps a state history and provide a way to navigate it.
Some vocabulary:
- **store**: A wrapper around data that is used to change said data and subscribe to these changes.
- **state**: The actual data that the store holds. It can be pretty much anything.
## Usage
### To create a shared store, use `of`
```ts
import { of } from '@known-as-bmf/store';
const store = of({
preferences: { theme: 'dark', lang: 'fr' },
lastOnline: '2020-02-21T18:22:33.343Z',
someArray: [],
});
```
`of` can also be passed a middleware as a second argument (see below).
### To read the current state, use `deref`
```ts
import { deref } from '@known-as-bmf/store';
const { preferences } = deref(store);
```
### To update the state, you can use `set` or `swap`
`set` totally replaces the current state with the provided value.
```ts
import { set } from '@known-as-bmf/store';
set(store, { preferences: {} });
```
With `swap`, you have to provide a function that computes the next state from the current one.
```ts
import { swap } from '@known-as-bmf/store';
swap(store, (s) => ({ ...s, lastOnline: new Date().toISOString() }));
```
We use [`immer`](https://github.com/immerjs/immer) under the hood, so you can even mutate the state given to you as argument in the update function.
```ts
import { swap } from '@known-as-bmf/store';
swap(store, (s) => {
s.preferences.theme = 'light';
s.someArray.push('hurray');
return s;
});
```
### To subscribe to state change, use `subscribe`
```ts
import { subscribe } from '@known-as-bmf/store';
// will be invoked when the state changes
subscribe(store, ({ previous, current }) => console.log(previous, current));
```
You can also provide a _selector_ function if you're only interested in a subset of the store. The store uses `===` to compare equality.
```ts
import { subscribe } from '@known-as-bmf/store';
// will only be invoked when `lastOnline` changes
subscribe(
store,
({ previous, current }) => console.log(previous, current),
(s) => s.lastOnline
);
```
`subscribe` returns an unsubscribe function you can call to stop listening to state changes.
```ts
import { subscribe } from '@known-as-bmf/store';
const unsubscribe = subscribe(store, ({ previous, current }) =>
console.log(previous, current)
);
unsubscribe();
```
### Observable
You can create an observable emitting state changes using [`@known-as-bmf/store-obs`](https://github.com/known-as-bmf/store/tree/master/packages/store-obs).
## Middlewares
You can register a middleware as second argument of `of`. If you need to register multiple middlewares, you can use `composeMiddlewares` or `pipeMiddlewares` to merge them (from _right-to-left_ and _left-to-right_ respectively). **Order of middlewares might be important !** They are invoked in order of registration.
Typically, a validation should take place before persisting the state.
Middlewares can use three hooks:
- `transformState` When the store is asked to change the state, this hook allows the middleware to transform the future state.
- `stateWillChange` Invoked when the state is about to change.
- `stateDidChange` Invoked when the state just changed.
This doc is still in progress but you can look at the typings for more information.
Some pre-existing middlewares:
- [@known-as-bmf/store-middleware-validator](https://github.com/known-as-bmf/store-middleware-validator) - validate / reject state updates.
- [@known-as-bmf/store-middleware-timetravel](https://github.com/known-as-bmf/store-middleware-timetravel) - undo / redo state changes.
- [@known-as-bmf/store-middleware-persist](https://github.com/known-as-bmf/store-middleware-persist) - persist state to browser storage.
## API
### of
```ts
/**
* Creates a store.
* @param initialState The initial value of the state.
* @param middleware Middleware to use for this store. You can compose multiple
* middlewares with `composeMiddlewares` and `pipeMiddlewares`.
*/
function of<S>(initialState: S, middleware?: Middleware<S>): Store<S>;
```
```ts
type Middleware<S> = (store: Store<S>, hooks: Hooks<S>) => void;
```
```ts
interface Hooks<S> {
/**
* Register a function that will be invoked each time a state change is requested.
* This function can transform the state and must return an array containing the new state.
* An array must be returned because of implementation details behind the scene.
* @param fn A state transformation function.
*/
transformState(fn: (state: Readonly<S>) => [S]): () => void;
/**
* Register a function that will be invoked when the state is about to change.
* It is invoked after all `transformState` hooks.
* @param fn A function invoked when the state is about to change.
*/
stateWillChange(fn: (state: Readonly<S>) => void): () => void;
/**
* Register a function that will be invoked after the state changed.
* It is invoked after all `transformState` and `stateWillChange` hooks.
* @param fn A function invoked when the state has changed.
*/
stateDidChange(fn: (state: Readonly<S>) => void): () => void;
}
```
### deref
```ts
/**
* Returns the current state of a store.
* @param store The store you want to get the current state from.
* @throws {TypeError} if the store is not a correct `Store` instance.
*/
function deref<S>(store: Store<S>): S;
```
### swap
```ts
/**
* Changes the state of a store using a function.
* @param store The store of which you want to change the state.
* @param mutationFn The function used to compute the value of the future state.
* @throws {TypeError} if the store is not a correct `Store` instance.
* @throws {Error} if the new state does not pass validation.
*/
function swap<S>(store: Store<S>, mutationFn: (current: S) => S): void;
```
### set
```ts
/**
* Changes the state of a store with a new one.
* @param store The store of which you want to change the state.
* @param newState The new state.
* @throws {TypeError} if the store is not a correct `Store` instance.
* @throws {Error} if the new state does not pass validation.
*/
function set<S>(store: Store<S>, newState: S): void;
```
### subscribe
```ts
/**
* Subscribes to state changes.
* @param store The store you want to subscribe to.
* @param callback The function to call when the state changes.
* @returns An unsubscribe function for this specific subscription.
* @throws {TypeError} if the store is not a correct `Store` instance.
*/
function subscribe<S>(
store: Store<S>,
callback: SubscriptionCallback<S>
): () => void;
/**
* Subscribes to state changes.
* @param store The store you want to subscribe to.
* @param callback The function to call when the state changes.
* @param selector The selector function, narrowing down the part of the state you want to subscribe to.
* @returns An unsubscribe function for this specific subscription.
* @throws {TypeError} if the store is not a correct `Store` instance.
*/
function subscribe<S, R>(
store: Store<S>,
callback: SubscriptionCallback<S>,
selector: Selector<S, R>
): () => void;
```
```ts
type SubscriptionCallback<S> = (event: StateChangedEvent<S>) => void;
```
```ts
type Selector<S, R> = (state: S) => R;
```
```ts
interface StateChangedEvent<S> {
/**
* The previous value of the state.
*/
previous: S;
/**
* The new value of the state.
*/
current: S;
}
```