mst-effect
Version:
Designed to be used with MobX-State-Tree to create asynchronous actions using RxJS.
192 lines (136 loc) • 8.57 kB
Markdown
<p align="center">
<a href="https://github.com/Runjuu/mst-effect">
<img width="150px" src="https://user-images.githubusercontent.com/12002941/111877518-b2c60900-89de-11eb-8f28-d5fd95897258.png" alt="mst-effect" />
</a>
</p>
<p align="center">
<a href="https://github.com/Runjuu/mst-effect/blob/main/LICENSE">
<img src="https://img.shields.io/npm/l/mst-effect?colorA=373737&colorB=0A70E9&style=flat" alt="GitHub license" />
</a>
<a href="https://www.npmjs.com/package/mst-effect">
<img src="https://img.shields.io/npm/v/mst-effect?colorA=373737&colorB=0A70E9&style=flat" alt="NPM version" />
</a>
<a href="https://bundlephobia.com/result?p=mst-effect">
<img src="https://img.shields.io/bundlephobia/min/mst-effect?label=bundle%20size&colorA=373737&colorB=0A70E9&style=flat" alt="Bundle size" />
</a>
<a href="https://coveralls.io/github/Runjuu/mst-effect?branch=main">
<img src="https://img.shields.io/coveralls/github/Runjuu/mst-effect?colorA=373737&colorB=0A70E9&style=flat" alt="Coverage Status" />
</a>
<a href="https://github.com/Runjuu/mst-effect/discussions">
<img src="https://img.shields.io/static/v1?label=discuss%20on&message=github&colorA=373737&colorB=0A70E9&style=flat" alt="Github discussions" />
</a>
</p>
`mst-effect` is designed to be used with <a href="https://github.com/mobxjs/mobx-state-tree">MobX-State-Tree</a> to create asynchronous actions using <a href="https://github.com/ReactiveX/rxjs">RxJS</a>. In case you haven't used them before:
> `MobX-State-Tree` is a full-featured reactive state management library that can **structure the state model** super intuitively.
> <br /> `RxJS` is a library for composing asynchronous and event-based programs that provides the best practice to **manage async codes**.
If you are still hesitant about learning `RxJS`, check the examples below and play around with them. I assure you that you'll be amazed by what it can do and how clean the code could be.
Already using `MobX-State-Tree`? Awesome! `mst-effect` is 100% compatible with your current project.
## Examples
- [Fetch data](https://codesandbox.io/s/fetch-data-i9hqb?file=/src/app.tsx)
- [Fetch with token](https://codesandbox.io/s/fetch-with-token-rbveh?file=/src/app.tsx)
- [Handle user input](https://codesandbox.io/s/handle-user-input-ef1pt?file=/src/app.tsx)
- [Mutually exclusive actions](https://codesandbox.io/s/mutually-exclusive-actions-ylqlf?file=/src/app.tsx)
- [Back pressure](https://codesandbox.io/s/backpressure-ulu1y?file=/src/app.tsx)
## Installation
`mst-effect` has peer dependencies of [mobx](https://www.npmjs.com/package/mobx), [mobx-state-tree](https://www.npmjs.com/package/mobx-state-tree) and [rxjs](https://www.npmjs.com/package/rxjs), which will have to be installed as well.
##### Using [yarn](https://yarnpkg.com/en/package/mst-effect):
```bash
yarn add mst-effect
```
##### Or via [npm](https://www.npmjs.com/package/mst-effect):
```bash
npm install mst-effect --save
```
## Basics
**`effect`** is the core method of `mst-effect`. It can automatically manage subscriptions and execute the emitted actions. For example:
```ts
import { types, effect, action } from 'mst-effect'
import { map, switchMap } from 'rxjs/operators'
const Model = types
.model({
value: types.string,
})
.actions((self) => ({
fetch: effect<string>(self, (payload$) => {
function setValue(value: string) {
self.value = value
}
return payload$.pipe(
switchMap((url) => fetch$(url)),
map((value) => action(setValue, value)),
)
}),
}))
```
#### Import location
As you can see in the example above, `types` need to be imported from `mst-effect`([Why?](#why-we-need-to-import-types-from-mst-effect)).
#### The definition of the `effect`
The first parameter is the model instance, as `effect` needs to unsubscribe the [`Observable`](https://rxjs-dev.firebaseapp.com/api/index/class/Observable) when the model is destroyed.
The second parameter, a factory function, can be thought of as the `Epic` of [redux-observable](https://redux-observable.js.org/docs/basics/Epics.html). The factory function is called only once at model creation. It takes a stream of payloads and returns a stream of actions. — **Payloads in, actions out.**
Finally, `effect` returns a function to feed a new value to the `payload$`. In actual implementation code, it's just an alias to `subject.next`.
#### What is `action`?
`action` can be considered roughly as a higher-order function that takes a callback function and the arguments for the callback function. But instead of executing immediately, it returns a new function. Action will be immediately invoked when emitted.
```ts
function action(callback, ...params): EffectAction {
return () => callback(...params)
}
```
## API Reference
### [👾](https://github.com/Runjuu/mst-effect/blob/main/src/effect/effect.ts) effect
`effect` is used to manage subscriptions automatically.
```ts
type ValidEffectActions = EffectAction | EffectAction[]
type EffectDispatcher<P> = (payload: P) => void
function effect<P>(
self: AnyInstance,
fn: (payload$: Observable<P>) => Observable<ValidEffectActions>,
): EffectDispatcher<P>
```
`payload$` emits data synchronously when the function returned by the effect is called. The returned `Observable<ValidEffectActions>` will automatically subscribed by `effect`
### [👾](https://github.com/Runjuu/mst-effect/blob/main/src/effect/doll-effect.ts) dollEffect
```ts
type ValidEffectActions = EffectAction | EffectAction[]
type DollEffectDispatcher<P, S> = <SS = S>(
payload: P,
handler?: (resolve$: Observable<S>) => Observable<SS>,
) => Promise<SS>
type SignalDispatcher<S> = (value: S) => void
function dollEffect<P, S>(
self: AnyInstance,
fn: (
payload$: Observable<P>,
signalDispatcher: SignalDispatcher<S>,
) => Observable<ValidEffectActions>,
): DollEffectDispatcher<P, S>
```
`dollEffect` is almost identical with `effect`. The primary difference is `DollEffectDispatcher` will return a `Promise` which is useful when you want to report some message to the caller. The `Promise` will fulfill when `SignalDispatcher` being invoked ([example](https://codesandbox.io/s/report-fetch-status-to-caller-d6gmh?file=/src/app.tsx)). Also, you can use the `handler` to control when and what the `Promise` should resolve ([example](https://codesandbox.io/s/report-fetch-status-to-caller-2-1eogq?file=/src/app.tsx)).
### [👾](https://github.com/Runjuu/mst-effect/blob/main/src/signal/index.ts) signal
```ts
export function signal<P, R = P>(
self: AnyInstance,
fn?: (payload$: Observable<P>) => Observable<R>,
): [Observable<R>, (payload: P) => void]
```
`signal` is an encapsulation of the [`Subject`](https://rxjs-dev.firebaseapp.com/api/index/class/Subject). You can use the second parameter to do some processing of the output data.
### [👾](https://github.com/Runjuu/mst-effect/blob/main/src/reaction$/index.ts) reaction$
```ts
export function reaction$<T>(
expression: (r: IReactionPublic) => T,
opts?: IReactionOptions,
): Observable<{ current: T; prev: T; r: IReactionPublic }>
```
`reaction$` encapsulates the [`reaction`](https://mobx.js.org/reactions.html#reaction) method from `mobx`. When the returned value changes, it will emit the corresponding data to the returned `Observable`.
## Recipes
#### Error Handling
When an error occurred in `Observable`, `effect` will re-subscribe the `Observable` (will not re-run the factory function). The common practice is to use the [`catchError`](https://rxjs-dev.firebaseapp.com/api/operators/catchError) operator for error handling. Check [fetch data](https://codesandbox.io/s/fetch-data-i9hqb?file=/src/app.tsx) example for more detail.
#### Cancellation
You can combine `signal` and [`takeUntil()`](https://rxjs-dev.firebaseapp.com/api/operators/takeUntil) operator to cancel an `Observable`. Check [mutually exclusive actions](https://codesandbox.io/s/mutually-exclusive-actions-ylqlf?file=/src/app.tsx) example for more detail.
## FAQ
#### Why we need to import `types` from `mst-effect`
Currently, `mobx-state-tree` does not support modifying the model outside of actions.
`mst-effect` overrides `types.model` so that the model can be modified in an asynchronous process.
Because `mst-effect` re-export all the variables and types in `mobx-state-tree`, you can simply change the import location to `mst-effect`.
```diff
- import { types, Instance } from 'mobx-state-tree'
+ import { types, Instance } from 'mst-effect'
```