UNPKG

eko-flobx

Version:

Flux inspired state management on top of MobX

120 lines (72 loc) 4.89 kB
# Flux inspired state management on top of MobX ## Installation 1. `npm install --save eko-flobx` or `yarn add eko-flobx` 2. `import * as flox from 'eko-flobx` ## Concepts ### StateStores A `StateStore` object represents a container to handle storage logic around a specific collection of data. It has a name which should be unique through your entire application, and should contains an observable object to store its data, usually a Map or a Set. ### Actions You can decorate its methods with 2 different _action_ decorators named `reducer` and `effect`: - A `reducer` action is usually made to mutate the internal store's dataset, and should only do that. It is synchronous by nature and should remain as atomic as possible. - An `effect` action is complementary to reducers and is meant to call external libraries or backends, ultimately resulting in calling a reducer action to store its result. It can be (and most of the time is) asynchronous. Marking a method with `@reducer` or `@effect` will register it in the global `dispatcher` object under the store's name, such as `dispatcher.storeName.method`. In addition to that, `@reducer` actions (only) are hookable from an other store through a dotted method writing (see examples below) ### dispatcher The `dispatcher` singleton is a super-set object built on top of the regular Flux dispatcher object. Its functions are simple: 1. Reference and expose stores and their reducers and effects in a single entry point, so you don't need to import stores in order to use them. This providers a loosely-coupled way to access a store's methods, although it does not guarantee that the store has been created, nor that the method exists. 2. Wrap calls of registered reducers functions and link them to Flux's standard `dispatch` mechanism (allowing for the global dotted notation). ## Examples ### Implementing a Store A simple example for a store is this one: ```javascript import { observable } from 'mobx'; import { StateStore, reducer, effect } from 'flobx'; export class UserStore extends StateStore { @observable users = new Map(); constructor() { super('user') } // effects handle asynchronous logic which may result in calling a reducer later on @effect async getUser({ userId }) { const { data } = await axios.get(`/user/${userId}`) dispatcher.user.load({ users: [data.user] }) } // reducers are only responsible for updating state. no side logic, they should always be synchronous. @reducer load({ users }) { users.forEach(user => this.users.set(user.id, user)) } } ``` #### Things to be noted: - The name of a store **must be unique** - You must always use `dispatcher` to call for an effect or a reducer. Standard calls with result in an error. ### Dispatching actions To call reducers and effects actions, you must always use the `dispatcher` singleton provided by the `flobx` package. Even if you can pass any type of data as argument, it is highly recommended to pass plain objects as payload and deconstruct them later on. ```javascript import { dispatcher } from 'flobx' // somewhere in code, whenever it is needed await dispatcher.user.getUser('user123'); ``` #### Things to be noted: - The `dispatcher.user` property will be populated at runtime, and is not guaranteed to be populated. The inclusion and creation of `UserStore` remains an other responsibility which you must implement yourself. Forgetting to do so will produce a silent `console.warn` warning for you to fix. - Similarly, the `dispatcher.user.getUser` method may or may not exist at runtime. Calling store's unknown method will produce a silent `console.warn` warning for you to fix. ### Global reducers A store can also react to an other store's reducer call using the reducer dotted notation. This allow for stores to react to one an other and create more complex structures. ```javascript import { StateStore, reducer } from 'flobx'; class Store1 extends StateStore { constructor() { super('store1') } @reducer load(payload) {} } class Store2 extends StateStore { constructor() { super('store2') } @reducer 'store1.load'(payload) {} } const store1 = new Store1() const store2 = new Store2() // ... dispatcher.store1.load() ``` #### Things to be noted: - If `store1` has never been instantiated at runtime, the store2's `store1.load` global reducer will become dead code since it's not possible to call it directly due to the rule of action decorators. - Reducers will be registered according to order of instantiation, meaning that `new Store1(); new Store2()` will produce a different stack call order than `new Store2(); new Store1()`. This should remain a minor side-effect since reducers should always be atomicly mutating their own store.