UNPKG

@known-as-bmf/store

Version:
263 lines (199 loc) 8.31 kB
# @known-as-bmf/store Lightweight synchronous state management library. <!-- [![Build Status](https://travis-ci.org/known-as-bmf/store.svg?branch=master)](https://travis-ci.org/known-as-bmf/store) [![Known Vulnerabilities](https://snyk.io/test/github/known-as-bmf/store/badge.svg?targetFile=package.json)](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; } ```