eko-flobx
Version:
Flux inspired state management on top of MobX
120 lines (72 loc) • 4.89 kB
Markdown
# 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.