@known-as-bmf/store
Lightweight synchronous state management library.
Installation
npm install --save @known-as-bmf/store
Description
This library is a reimplementation from scratch of @libre/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
of
To create a shared store, use 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).
deref
To read the current state, use import { deref } from '@known-as-bmf/store';
const { preferences } = deref(store);
set
or swap
To update the state, you can use set
totally replaces the current state with the provided value.
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.
import { swap } from '@known-as-bmf/store';
swap(store, (s) => ({ ...s, lastOnline: new Date().toISOString() }));
We use immer
under the hood, so you can even mutate the state given to you as argument in the update function.
import { swap } from '@known-as-bmf/store';
swap(store, (s) => {
s.preferences.theme = 'light';
s.someArray.push('hurray');
return s;
});
subscribe
To subscribe to state change, use 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.
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.
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
.
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 - validate / reject state updates.
- @known-as-bmf/store-middleware-timetravel - undo / redo state changes.
- @known-as-bmf/store-middleware-persist - persist state to browser storage.
API
of
/**
* 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>;
type Middleware<S> = (store: Store<S>, hooks: Hooks<S>) => void;
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
/**
* 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
/**
* 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
/**
* 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
/**
* 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;
type SubscriptionCallback<S> = (event: StateChangedEvent<S>) => void;
type Selector<S, R> = (state: S) => R;
interface StateChangedEvent<S> {
/**
* The previous value of the state.
*/
previous: S;
/**
* The new value of the state.
*/
current: S;
}