1 | ![Module](https://img.shields.io/badge/%40platform-state-%23EA4E7E.svg)
|
2 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
|
3 | [![NPM](https://img.shields.io/npm/v/@platform/state.svg?colorB=blue&style=flat)](https://www.npmjs.com/package/@platform/state)
|
4 | ![banner](https://user-images.githubusercontent.com/185555/82968704-09aebc00-a022-11ea-9222-a334ef10b426.png)
|
5 |
|
6 | A small, simple [rx/observable](https://github.com/ReactiveX/rxjs) based state-machine.
|
7 | For applying to [UI](https://en.wikipedia.org/wiki/User_interface) see the [react](https://reactjs.org) bindings at [`@platform/state.react`](../state.react)
|
8 |
|
9 | <p> <p>
|
10 |
|
11 | ## Install
|
12 |
|
13 | yarn add @platform/state
|
14 |
|
15 | To work abstractly with a state library use the [isolated type library](../state.types):
|
16 |
|
17 | yarn add @platform/state.types
|
18 |
|
19 | <p> <p>
|
20 |
|
21 | ## Getting Started
|
22 |
|
23 | Define your `model` and mutation `events`:
|
24 |
|
25 | ```typescript
|
26 | type IMyModel = {
|
27 | count: number;
|
28 | };
|
29 |
|
30 | type MyEvent = IIncrementEvent | IDecrementEvent;
|
31 | type IIncrementEvent = { type: 'TEST/increment'; payload: { by: number } };
|
32 | type IDecrementEvent = { type: 'TEST/decrement'; payload: { by: number } };
|
33 | type IStatustEvent = { type: 'TEST/status'; payload: { status: string } };
|
34 | ```
|
35 |
|
36 | <p> <p>
|
37 |
|
38 | Create a new state-machine:
|
39 |
|
40 | ```typescript
|
41 | import { Store } from '@platform/state';
|
42 |
|
43 | const initial: IMyModel = { count: 0, status: string };
|
44 | const store = Store.create<IMyModel, MyEvent>({ initial });
|
45 | ```
|
46 |
|
47 | <p> <p>
|
48 |
|
49 | Define a listener that mutates the state based on a specific event type (equivalent to a ["reducer"](https://redux.js.org/basics/reducers)):
|
50 |
|
51 | ```typescript
|
52 | store.on<ITestIncrementEvent>('TEST/increment').subscribe((e) => {
|
53 | const count = e.state.count + e.payload.by;
|
54 | const next = { ...e.state, count };
|
55 | e.change(next); // UPDATE: New copy of state applied.
|
56 | });
|
57 | ```
|
58 |
|
59 | alternatively you can use "mutation like" syntax by passing a change function:
|
60 |
|
61 | ```typescript
|
62 | store
|
63 | .on<ITestIncrementEvent>('TEST/increment')
|
64 | .subscribe(e => {
|
65 | e.change(draft => {
|
66 | draft.count += e.payload.by; // UPDATE: New "structurally shared" immutable changes applied.
|
67 | }));
|
68 | });
|
69 | ```
|
70 |
|
71 | This safely modifies an immutable clone of the state using "structural sharing" for efficiency (via [immer](https://immerjs.github.io/immer)).
|
72 |
|
73 | <p> <p>
|
74 |
|
75 | Dispatch events to change state:
|
76 |
|
77 | ```typescript
|
78 | store.state; // => count === 0
|
79 | store.dispatch({ type: 'TEST/increment', payload: { by: 1 } });
|
80 | store.state; // => count === 1
|
81 | ```
|
82 |
|
83 | <p> <p>
|
84 |
|
85 | Listen for changes to the state and react accordingly, for instance updating UI that may be rendering the state.:
|
86 |
|
87 | ```typescript
|
88 | store.changed$.subscribe((e) => {
|
89 | // ...
|
90 | });
|
91 | ```
|
92 |
|
93 | <p> <p>
|
94 |
|
95 | Add logic that reacts to events asynchronously and dispatches new update events (equivalent to an ["epic"](https://redux-observable.js.org)):
|
96 |
|
97 | ```typescript
|
98 | store
|
99 | .on<IIncrementEvent>('TEST/increment')
|
100 | .pipe(debounceTime(300))
|
101 | .subscribe(async (e) => {
|
102 | const status = await getNetworkStatus();
|
103 | e.dispatch({ type: 'TEST/status', payload: { status } });
|
104 | });
|
105 | ```
|
106 |
|
107 | <p> <p>
|
108 | <p> <p>
|