state-decorator 6.13.0 → 7.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +552 -201
- package/es/helpers.d.ts +20 -2
- package/es/helpers.js +1 -1
- package/es/helpers.js.map +1 -1
- package/es/impl.d.ts +1 -1
- package/es/impl.js +1 -1
- package/es/index.d.ts +57 -43
- package/es/index.js +1 -1
- package/es/index.js.map +1 -1
- package/es/middlewares.js +1 -1
- package/es/test.d.ts +2 -2
- package/es/test.js +1 -1
- package/es/test.js.map +1 -1
- package/es/types.d.ts +21 -10
- package/es/v5.js +1 -1
- package/es/v5.js.map +1 -1
- package/es/v5_test.js +1 -1
- package/helpers.d.ts +20 -2
- package/helpers.js +1 -1
- package/helpers.js.map +1 -1
- package/impl.d.ts +1 -1
- package/impl.js +1 -1
- package/index.d.ts +57 -43
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/middlewares.js +1 -1
- package/package.json +1 -1
- package/test.d.ts +2 -2
- package/test.js +1 -1
- package/test.js.map +1 -1
- package/types.d.ts +21 -10
- package/v5.js +1 -1
- package/v5.js.map +1 -1
- package/v5_test.js +1 -1
package/README.md
CHANGED
|
@@ -11,12 +11,22 @@ The StateDecorator is a set of Reacts hook that manages a complex component stat
|
|
|
11
11
|
- Deterministic state changes (clear separation of effects and side effects)
|
|
12
12
|
- Ease asynchronous actions state changes (loading / error states, success & error handlers, parallel actions management, optimistic updates...)
|
|
13
13
|
- Easily and efficiently share slices of state.
|
|
14
|
-
- Easily testable (
|
|
14
|
+
- Easily testable (test framework provided)
|
|
15
15
|
- Easily update state from or react to props changes
|
|
16
|
-
- Ease debugging (trace state changes)
|
|
16
|
+
- Ease debugging (trace state changes, dev tools)
|
|
17
17
|
- Improve code conciseness (no boiler plate code)
|
|
18
|
-
-
|
|
19
|
-
|
|
18
|
+
- Strongly typed (Typescript)
|
|
19
|
+
|
|
20
|
+
# V7: Partial state and new hooks
|
|
21
|
+
|
|
22
|
+
- React 17 (upcoming V8 for React 18)
|
|
23
|
+
- All effects can now return a partial state that will be merged with current state.
|
|
24
|
+
- More documentation on global and local store use cases.
|
|
25
|
+
- **useLocalStore** and **useStore** now have a parameter to specify if the React component must re-rerender after a state change.
|
|
26
|
+
- New **useStoreContextSlice** hook was added to get a slice of a store in a context.
|
|
27
|
+
- New **onMountDeferred** to execute initialization code after initial render
|
|
28
|
+
- New effect helpers
|
|
29
|
+
- Removed useBindStore hook.
|
|
20
30
|
|
|
21
31
|
# 🏎️ V6: The Store
|
|
22
32
|
|
|
@@ -48,8 +58,7 @@ yarn add state-decorator
|
|
|
48
58
|
### Example
|
|
49
59
|
|
|
50
60
|
```typescript
|
|
51
|
-
import
|
|
52
|
-
import { useLocalStore, StoreActions } from 'state-decorator';
|
|
61
|
+
import { useLocalStore, StoreConfig } from "state-decorator";
|
|
53
62
|
|
|
54
63
|
// Typings
|
|
55
64
|
|
|
@@ -74,46 +83,48 @@ type Props = {
|
|
|
74
83
|
};
|
|
75
84
|
|
|
76
85
|
// Initial state & actions
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
87
|
+
getInitialState: () => ({
|
|
88
|
+
selectedUserId: null,
|
|
89
|
+
user: null,
|
|
90
|
+
}),
|
|
91
|
+
|
|
92
|
+
actions: {
|
|
93
|
+
// synchronous action (simple form)
|
|
94
|
+
selectUser: ({ args: [id] }) => ({ selectedUserId: id }),
|
|
95
|
+
|
|
96
|
+
// a synchronous action with side effects
|
|
97
|
+
selectAndLoadUser: {
|
|
98
|
+
// effects contain only immutable state changes
|
|
99
|
+
effects: ({ args: [id] }) => ({ selectedUserId: id }),
|
|
100
|
+
// side effects (ie. other effects than state changes), using "actions" alias
|
|
101
|
+
sideEffects: ({ a }) => {
|
|
102
|
+
a.loadUser();
|
|
103
|
+
},
|
|
95
104
|
},
|
|
96
|
-
},
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
// asynchronous action
|
|
107
|
+
loadUser: {
|
|
108
|
+
// promise provider, can use action context (ctx) to get current state, args, props etc.
|
|
109
|
+
getPromise: (ctx) =>
|
|
110
|
+
Promise.resolve({ firstName: "John", lastName: "Doe" }),
|
|
111
|
+
// state changes
|
|
112
|
+
effects: ({ res }) => ({ user: res }),
|
|
113
|
+
// state changes if error
|
|
114
|
+
errorEffects: ({ error }) => ({ user: null }),
|
|
115
|
+
// side effects here
|
|
116
|
+
sideEffects: ({ res, actions }) => {},
|
|
117
|
+
// side effects if error
|
|
118
|
+
errorSideEffects: ({ err, actions }) => {},
|
|
119
|
+
// many more options available here, read the doc!
|
|
120
|
+
},
|
|
111
121
|
},
|
|
112
122
|
};
|
|
113
123
|
|
|
124
|
+
|
|
114
125
|
// Bind to react component and use state/actions
|
|
115
126
|
export function App(props: Props) => {
|
|
116
|
-
const { state, actions } = useLocalStore(
|
|
127
|
+
const { state, actions } = useLocalStore(config);
|
|
117
128
|
return (
|
|
118
129
|
<div>
|
|
119
130
|
{state.user && (
|
|
@@ -139,24 +150,25 @@ export function App(props: Props) => {
|
|
|
139
150
|
|
|
140
151
|
# React hooks
|
|
141
152
|
|
|
142
|
-
| Hook
|
|
143
|
-
|
|
|
144
|
-
| useLocalStore
|
|
145
|
-
| useStore
|
|
146
|
-
|
|
|
147
|
-
|
|
|
153
|
+
| Hook | Purpose | Returns | Is component refreshed on store change? | Is store destroyed on unmount? |
|
|
154
|
+
| -------------------- | --------------------------------------------------- | ------- | --------------------------------------- | ------------------------------ |
|
|
155
|
+
| useLocalStore | Create a store and binds it to the react component. | Store | if **refreshOnUpdate** is set | Y |
|
|
156
|
+
| useStore | Binds a store instance to the react component. | Store | if **refreshOnUpdate** is set | N |
|
|
157
|
+
| useStoreSlice | Get a slice of a store. | Slice | If slice has changed only | N |
|
|
158
|
+
| useStoreContextSlice | Get a slice of a store stored in a context | Slice | If slice has changed only | N |
|
|
148
159
|
|
|
149
160
|
# Initial state
|
|
150
161
|
|
|
151
162
|
The initial state is build using a function provided to the store.
|
|
152
163
|
|
|
153
164
|
```typescript
|
|
154
|
-
|
|
155
|
-
counter: 0,
|
|
156
|
-
}
|
|
165
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
166
|
+
getInitialState: (p) => ({ counter: 0 }),
|
|
167
|
+
actions: {},
|
|
168
|
+
};
|
|
157
169
|
|
|
158
170
|
export const CounterContainer = () => {
|
|
159
|
-
const { state, actions } = useLocalStore(
|
|
171
|
+
const { state, actions } = useLocalStore(config);
|
|
160
172
|
return <CounterView {...state} {...actions} />;
|
|
161
173
|
};
|
|
162
174
|
```
|
|
@@ -191,14 +203,16 @@ The StateDecorator is taking a list of actions and decorate them to inject state
|
|
|
191
203
|
## Short form
|
|
192
204
|
|
|
193
205
|
```typescript
|
|
194
|
-
action: (ctx) => ctx.state, // state
|
|
206
|
+
action: (ctx) => ctx.state, // changes to be merged to current state
|
|
195
207
|
```
|
|
196
208
|
|
|
197
209
|
Example:
|
|
198
210
|
|
|
199
211
|
```typescript
|
|
200
|
-
const
|
|
201
|
-
|
|
212
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
213
|
+
actions: {
|
|
214
|
+
setText: ({ args: [newText] }) => ({ text: newText }),
|
|
215
|
+
},
|
|
202
216
|
};
|
|
203
217
|
```
|
|
204
218
|
|
|
@@ -221,14 +235,16 @@ action: {
|
|
|
221
235
|
Example:
|
|
222
236
|
|
|
223
237
|
```typescript
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
a
|
|
238
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
239
|
+
actions: {
|
|
240
|
+
setText: {
|
|
241
|
+
effects: ({ args: [newText] }) => ({ text: newText }),
|
|
242
|
+
// use either debounceTimeout or debounceSideEffectsTimeout
|
|
243
|
+
// debounceTimeout: 5000,
|
|
244
|
+
debounceSideEffectsTimeout: 5000,
|
|
245
|
+
sideEffects: ({ a }) => {
|
|
246
|
+
a.saveDocument();
|
|
247
|
+
},
|
|
232
248
|
},
|
|
233
249
|
},
|
|
234
250
|
};
|
|
@@ -270,14 +286,16 @@ action: {
|
|
|
270
286
|
Example:
|
|
271
287
|
|
|
272
288
|
```typescript
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
a
|
|
289
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
290
|
+
actions: {
|
|
291
|
+
loadList: {
|
|
292
|
+
getPromise: () => fetch('https://myapi/myservices').then((res) => (res.ok ? res.json() : Promise.reject())),
|
|
293
|
+
effects: ({ res }) => ({ list: res }),
|
|
294
|
+
sideEffects: ({ a }) => {
|
|
295
|
+
a.otherAction();
|
|
296
|
+
},
|
|
297
|
+
getErrorMessage: () => 'Cannot load list',
|
|
279
298
|
},
|
|
280
|
-
getErrorMessage: () => 'Cannot load list',
|
|
281
299
|
},
|
|
282
300
|
};
|
|
283
301
|
```
|
|
@@ -375,11 +393,13 @@ const store = useLocalStore(getInitialState, actionsImpl, props, {
|
|
|
375
393
|
|
|
376
394
|
```typescript
|
|
377
395
|
// action implementation
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
396
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
397
|
+
actions: {
|
|
398
|
+
loadList: {
|
|
399
|
+
getPromise: () => new Promise((_, reject) => setTimeout(reject, 500, new Error('Too bad'))),
|
|
400
|
+
getSuccessMessage: () => 'success!',
|
|
401
|
+
getErrorMessage: ({ err }) => `Error: ${err.message}`,
|
|
402
|
+
},
|
|
383
403
|
},
|
|
384
404
|
};
|
|
385
405
|
```
|
|
@@ -396,7 +416,7 @@ If some actions need to be triggered, set **errorSideEffects**.
|
|
|
396
416
|
const actions: StoreActions<State, Actions> = {
|
|
397
417
|
loadList: {
|
|
398
418
|
getPromise: () => new Promise((_, reject) => setTimeout(reject, 500, new Error('Too bad'))),
|
|
399
|
-
errorEffects: ({
|
|
419
|
+
errorEffects: ({ error }) => ({ error: error.message, list: [] }),
|
|
400
420
|
getErrorMessage: ({ err }) => err.message,
|
|
401
421
|
errorSideEffects: ({ error }) => {
|
|
402
422
|
Logger.log(error);
|
|
@@ -496,10 +516,20 @@ setGlobalConfig({
|
|
|
496
516
|
On a specific store:
|
|
497
517
|
|
|
498
518
|
```typescript
|
|
499
|
-
import { createStore } from 'state-decorator';
|
|
519
|
+
import { createStore, StoreConfig } from 'state-decorator';
|
|
500
520
|
import { optimisticActions } from 'state-decorator/middlewares';
|
|
501
521
|
|
|
502
|
-
const
|
|
522
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
523
|
+
getInitialState: () => ({
|
|
524
|
+
/* ... */
|
|
525
|
+
}),
|
|
526
|
+
actions: {
|
|
527
|
+
/* ... */
|
|
528
|
+
},
|
|
529
|
+
middlewares: [optimisticActions()],
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const store = createStore(config);
|
|
503
533
|
```
|
|
504
534
|
|
|
505
535
|
An optimistic action assumes that the action will, most of the time, succeed. So it will apply the effects as soon as the asynchronous action is called (as opposite to the regular effects which are applied when the promise is resolved).
|
|
@@ -517,11 +547,13 @@ The undo strategy is the following:
|
|
|
517
547
|
Example:
|
|
518
548
|
|
|
519
549
|
```typescript
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
550
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
551
|
+
actions: {
|
|
552
|
+
deleteItem: {
|
|
553
|
+
getPromise: ([id]) => new Promise((resolve) => setTimeout(resolve, 500)),
|
|
554
|
+
optimisticEffects: ({ args: [id] }) => ({ list: list.filter((item) => item.id === id) }),
|
|
555
|
+
errorEffects: ({err} => ({ err: error.message }),
|
|
556
|
+
},
|
|
525
557
|
},
|
|
526
558
|
};
|
|
527
559
|
```
|
|
@@ -545,26 +577,28 @@ Ongoing asynchronous actions can be aborted using, under the hood, the [AbortCon
|
|
|
545
577
|
3. An abort is a specific failure of an action, ie. **errorEffects** and **errorSideEffects** will be called. Use the error type and name to distinguish an aborted action from a regular failed action.
|
|
546
578
|
|
|
547
579
|
```typescript
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
580
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
581
|
+
actions: {
|
|
582
|
+
onAction: {
|
|
583
|
+
abortable: true,
|
|
584
|
+
preEffects: () => ({ isError: false, isSuccess: false, isAborted: false }),
|
|
585
|
+
getPromise: ({s, args: [willCrash] abortSignal}) =>
|
|
586
|
+
new Promise((resolve, reject) => {
|
|
587
|
+
const timeout = window.setTimeout(willCrash ? reject : resolve, 2500, willCrash ? new Error('boom') : 'result');
|
|
588
|
+
abortSignal.addEventListener('abort', () => {
|
|
589
|
+
window.clearTimeout(timeout);
|
|
590
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
591
|
+
});
|
|
592
|
+
}),
|
|
593
|
+
errorEffects: ({ err}) => (err.name === 'AbortError' ? { isAborted: true } : { isError: true }),
|
|
594
|
+
effects: () => ({ isSuccess: true }),
|
|
595
|
+
errorSideEffects: ({ err }) => {
|
|
596
|
+
if (e.name === 'AbortError') {
|
|
597
|
+
console.log('AbortError side effect');
|
|
598
|
+
} else {
|
|
599
|
+
console.log('Other error side effect');
|
|
600
|
+
}
|
|
601
|
+
},
|
|
568
602
|
},
|
|
569
603
|
},
|
|
570
604
|
};
|
|
@@ -574,18 +608,72 @@ Note: ongoing abortable actions will be automatically aborted on store destructi
|
|
|
574
608
|
|
|
575
609
|
[](https://codesandbox.io/s/abort-v6-uvnl7)
|
|
576
610
|
|
|
611
|
+
## Helper effect functions
|
|
612
|
+
|
|
613
|
+
For common / simple use cases, some helpers functions are provided to write basic action effects and improve actions readability.
|
|
614
|
+
|
|
615
|
+
Example
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
import { setArgIn } from 'state-decorator/helpers';
|
|
619
|
+
|
|
620
|
+
type State = {
|
|
621
|
+
myProp: string;
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
type Actions = {
|
|
625
|
+
setMyProp: (v: string) => void;
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const actions: StoreActions<State, Actions> = {
|
|
629
|
+
setMyProp: setArgIn('myProp'),
|
|
630
|
+
// same as
|
|
631
|
+
// setMyProp: ({ args: [v] }) => ({ myProp: v }),
|
|
632
|
+
};
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
| Name | Purpose | Action signatue |
|
|
636
|
+
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
|
|
637
|
+
| setArgIn | Effect function to update a state property from action first argument | (v:T) => void |
|
|
638
|
+
| setTrueIn | Effect function to update a boolean state property to true | () => void |
|
|
639
|
+
| setFalseIn | Effect function to update a boolean state property to false | () => void |
|
|
640
|
+
| toggleProp | Effect function to toggle a boolean state property | () => void |
|
|
641
|
+
| setArgsInMap | Effect function to update a state property of type Record<string, T> using first action argument as key and second as value | (id: string, v:T) => void |
|
|
642
|
+
| setArgsInArray | Effect function to update a state property of type T[] using first action argument as index and second as value | (index: number, v:T) => void |
|
|
643
|
+
| setResIn | Effect function of asynchronous action to update a state property with the result of the promise | () => Promise<T> |
|
|
644
|
+
| setResInArray | Effect function to update a state property of type T[] using index from first action argument as index and as value the result of the promise | (index: number) => Promise <T> |
|
|
645
|
+
| setResInMap | Effect function to update a state property of type Record<string, T> using key from first action argument as index and as value the result of the promise | (id: string) => Promise <T> |
|
|
646
|
+
|
|
577
647
|
## Call actions on mount
|
|
578
648
|
|
|
579
649
|
When store is created (for example whe React component in mounted), the **onMount** option is called.
|
|
580
650
|
|
|
581
651
|
```typescript
|
|
582
|
-
import {
|
|
652
|
+
import { StoreConfig } from 'state-decorator';
|
|
583
653
|
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
654
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
655
|
+
getInitialState,
|
|
656
|
+
actions,
|
|
657
|
+
onMount: ({ p }) => {
|
|
658
|
+
p.loadParentList();
|
|
587
659
|
},
|
|
588
|
-
}
|
|
660
|
+
};
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
After React component is mounted using _useLocalStore_, the **onMountDeferred** option is called (in a _useEffect_).
|
|
664
|
+
The **onMount** is called _during_ the render of the hooks. If the onMount code is containing a function that is changing parent state,
|
|
665
|
+
React will not update the parent state and will issue a warning.
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
import { StoreConfig } from 'state-decorator';
|
|
669
|
+
|
|
670
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
671
|
+
getInitialState,
|
|
672
|
+
actions,
|
|
673
|
+
onMountDeferred: ({ p }) => {
|
|
674
|
+
p.loadParentList();
|
|
675
|
+
},
|
|
676
|
+
};
|
|
589
677
|
```
|
|
590
678
|
|
|
591
679
|
If an initial action is launched each time a property changes, consider using the **onMount** flag of the **onPropsChange** entry.
|
|
@@ -595,14 +683,16 @@ If an initial action is launched each time a property changes, consider using th
|
|
|
595
683
|
When the store is destroyed (for example on React component unmounted), the **onUnmount** option is called.
|
|
596
684
|
|
|
597
685
|
```typescript
|
|
598
|
-
import {
|
|
686
|
+
import { StoreConfig } from 'state-decorator';
|
|
599
687
|
|
|
600
|
-
const
|
|
688
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
689
|
+
getInitialState,
|
|
690
|
+
actions,
|
|
601
691
|
onUnmount: ({ s, abortedActions }) => {
|
|
602
692
|
// ex: store s in local storage
|
|
603
693
|
// ex: clean cache of aborted actions
|
|
604
694
|
},
|
|
605
|
-
}
|
|
695
|
+
};
|
|
606
696
|
```
|
|
607
697
|
|
|
608
698
|
## State sharing and slices
|
|
@@ -619,7 +709,7 @@ import { useStore, useStoreSlice, StoreActions } from 'state-decorator';
|
|
|
619
709
|
|
|
620
710
|
// Create a store
|
|
621
711
|
|
|
622
|
-
export const store = createStore(
|
|
712
|
+
export const store = createStore(config);
|
|
623
713
|
|
|
624
714
|
// Bind to react component
|
|
625
715
|
|
|
@@ -649,34 +739,6 @@ export function SubComponent() {
|
|
|
649
739
|
|
|
650
740
|
[](https://codesandbox.io/s/slices-v6-eg471?file=/src/SliceView.tsx)
|
|
651
741
|
|
|
652
|
-
# Global configuration
|
|
653
|
-
|
|
654
|
-
Overrides configuration to set properties that will be used by all stores.
|
|
655
|
-
|
|
656
|
-
```typescript
|
|
657
|
-
// all parameters are optional
|
|
658
|
-
setGlobalConfig({
|
|
659
|
-
// Used to clone state and props when managing optimistic conflicting actions.
|
|
660
|
-
clone: defaultCloneFunc,
|
|
661
|
-
// Compare to objets and returns if they are equal (slices).
|
|
662
|
-
comparator: shallow,
|
|
663
|
-
// Callback function to handle asychronous actions rejected promise (error reporting).
|
|
664
|
-
asyncErrorHandler: () => {},
|
|
665
|
-
// Tests if the error will trigger a retry of the action or will fail directly (retry promises)
|
|
666
|
-
retryOnErrorFunction: (error: Error) => error instanceof TypeError,
|
|
667
|
-
// Notification function on successful asynchronous action (if success message is set on action)
|
|
668
|
-
notifySuccess: undefined,
|
|
669
|
-
// Notification function on failed asynchronous action (if error message is set on action)
|
|
670
|
-
notifyError: undefined,
|
|
671
|
-
// Notification function injected in side effects action context (success and error) to notify warning
|
|
672
|
-
notifyWarning: undefined,
|
|
673
|
-
// Function called to return common error message if error message is not provided or error not managed in action.
|
|
674
|
-
// To override locally in action, provide a function that returns _null_.
|
|
675
|
-
// If _isErrorManaged_ is set to _true_, this function will not be called.
|
|
676
|
-
getErrorMessage: undefined,
|
|
677
|
-
});
|
|
678
|
-
```
|
|
679
|
-
|
|
680
742
|
# Update store when props change
|
|
681
743
|
|
|
682
744
|
## Getting started
|
|
@@ -684,12 +746,14 @@ setGlobalConfig({
|
|
|
684
746
|
```typescript
|
|
685
747
|
import { createStore } from 'state-decorator';
|
|
686
748
|
|
|
687
|
-
const
|
|
749
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
750
|
+
getInitialState,
|
|
751
|
+
actions,
|
|
688
752
|
onPropsChange: {
|
|
689
753
|
// list of depedencies that should trigger effects & side effects if changed
|
|
690
754
|
getDeps: (p) => [p.id],
|
|
691
755
|
// state changes, indices contains the indices of dependencies that have changed
|
|
692
|
-
effects: ({
|
|
756
|
+
effects: ({ props, indices, isInit }) => ({ selectedId: props.id }),
|
|
693
757
|
// other changes (use short aliases)
|
|
694
758
|
sideEffects: ({ p, a, indices, isInit }) => {
|
|
695
759
|
a.loadItem(p.id);
|
|
@@ -697,7 +761,7 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
697
761
|
// whether apply effects & side effects on mount
|
|
698
762
|
onMount: true,
|
|
699
763
|
},
|
|
700
|
-
}
|
|
764
|
+
};
|
|
701
765
|
```
|
|
702
766
|
|
|
703
767
|
## Multiple props change configuration
|
|
@@ -709,9 +773,11 @@ The store can manage several props change configurations to have a better granul
|
|
|
709
773
|
- Side effects are invoked after all effects using computed state
|
|
710
774
|
|
|
711
775
|
```typescript
|
|
712
|
-
import {
|
|
776
|
+
import { StoreConfig } from 'state-decorator';
|
|
713
777
|
|
|
714
|
-
const
|
|
778
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
779
|
+
getInitialState,
|
|
780
|
+
actions,
|
|
715
781
|
onPropsChange: [
|
|
716
782
|
{
|
|
717
783
|
getDeps: (p) => [p.id],
|
|
@@ -725,13 +791,13 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
725
791
|
},
|
|
726
792
|
{
|
|
727
793
|
getDeps: (p) => [p.otherProp],
|
|
728
|
-
effects: ({
|
|
794
|
+
effects: ({ props, indices }) => ({ otherProp: props.otherProp }),
|
|
729
795
|
sideEffects: ({ p, a }) => {
|
|
730
796
|
a.otherSideEffect(p.otherProp);
|
|
731
797
|
},
|
|
732
798
|
},
|
|
733
799
|
],
|
|
734
|
-
}
|
|
800
|
+
};
|
|
735
801
|
```
|
|
736
802
|
|
|
737
803
|
## OnMount
|
|
@@ -741,35 +807,43 @@ If the onMount flag is set on the props change configuration, the effects and si
|
|
|
741
807
|
It allows to trigger same actions at creation time and when the dependencies props are changed.
|
|
742
808
|
|
|
743
809
|
```typescript
|
|
744
|
-
|
|
810
|
+
import { StoreConfig } from 'state-decorator';
|
|
811
|
+
|
|
812
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
813
|
+
getInitialState,
|
|
814
|
+
actions,
|
|
745
815
|
onMount: ({ p }) => {
|
|
746
816
|
a.loadUser(p.userId);
|
|
747
817
|
},
|
|
748
818
|
onPropsChange: [
|
|
749
819
|
{
|
|
750
820
|
getDeps: (p) => [p.userId],
|
|
751
|
-
effects: ({
|
|
821
|
+
effects: ({ p }) => ({ userId: p.userId }),
|
|
752
822
|
sideEffects: ({ p, a }) => {
|
|
753
823
|
a.loadUser(p.userId);
|
|
754
824
|
},
|
|
755
825
|
},
|
|
756
826
|
],
|
|
757
|
-
}
|
|
827
|
+
};
|
|
758
828
|
|
|
759
829
|
// SAME AS
|
|
760
830
|
|
|
761
|
-
|
|
831
|
+
import { StoreConfig } from 'state-decorator';
|
|
832
|
+
|
|
833
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
834
|
+
getInitialState,
|
|
835
|
+
actions,
|
|
762
836
|
onPropsChange: [
|
|
763
837
|
{
|
|
764
838
|
getDeps: (p) => [p.userId],
|
|
765
|
-
effects: ({
|
|
839
|
+
effects: ({ p, isInit }) => (isInit ? null : { userId: p.userId }),
|
|
766
840
|
sideEffects: ({ p, a }) => {
|
|
767
841
|
a.loadUser(p.userId);
|
|
768
842
|
},
|
|
769
843
|
onMount: true,
|
|
770
844
|
},
|
|
771
845
|
],
|
|
772
|
-
}
|
|
846
|
+
};
|
|
773
847
|
```
|
|
774
848
|
|
|
775
849
|
**Caution**: the effects are executed too. Use the isInit parameter is passed to the context to apply or not state change.
|
|
@@ -779,6 +853,210 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
779
853
|
- Use several prop change configurations to separate dependencies between props and have simple definition.
|
|
780
854
|
- Use onMount flag to have a more systematic and simpler code.
|
|
781
855
|
|
|
856
|
+
# Global / local Stores and State sharing
|
|
857
|
+
|
|
858
|
+
There are two ways to share state and actions:
|
|
859
|
+
|
|
860
|
+
- Using regular props.
|
|
861
|
+
- Using state slices.
|
|
862
|
+
|
|
863
|
+
There are two types of stores:
|
|
864
|
+
|
|
865
|
+
- Global store:
|
|
866
|
+
|
|
867
|
+
- store is created as an instance (**createStore**) and it is exported from the JS module.
|
|
868
|
+
- it can be imported in any other JS module.
|
|
869
|
+
- store lifespan is equal to the application one.
|
|
870
|
+
|
|
871
|
+
- Local store:
|
|
872
|
+
- store is created by a hook (**useLocalStore**) and is bound to a React component (each instance of this component owns its own store).
|
|
873
|
+
- action and props can be shared using props or sharing the store itself (see example below)
|
|
874
|
+
- store is destroyed when its owner React component is unmounted.
|
|
875
|
+
|
|
876
|
+
## Using props
|
|
877
|
+
|
|
878
|
+
- Bind a React component to make store state / actions available on React component.
|
|
879
|
+
- Any change in the store (state, loading actions, ...) will trigger a refresh of the bound React component.
|
|
880
|
+
- Pass state / actions using regular React props.
|
|
881
|
+
- Use React.memo to prevent unecessary React re-renders if needed.
|
|
882
|
+
|
|
883
|
+
### Global store
|
|
884
|
+
|
|
885
|
+
```typescript
|
|
886
|
+
// Declare typings & actions as above
|
|
887
|
+
|
|
888
|
+
// Create a store in a dedicated file, named GlobalStore.ts for example
|
|
889
|
+
export const store = createStore(getInitialState, userAppActions);
|
|
890
|
+
|
|
891
|
+
//----------------------
|
|
892
|
+
|
|
893
|
+
// In another file
|
|
894
|
+
import React, { memo } from 'react';
|
|
895
|
+
import { useStore } from 'state-decorator';
|
|
896
|
+
import store from './GlobalStore';
|
|
897
|
+
|
|
898
|
+
// Bind to react component
|
|
899
|
+
// Each time the store state changes, a re-render is done.
|
|
900
|
+
export function Container(props: Props) {
|
|
901
|
+
const { state, actions } = useStore(store, props);
|
|
902
|
+
return (
|
|
903
|
+
<div>
|
|
904
|
+
<Title text={state.title} />
|
|
905
|
+
<Subtitle text={state.subtitle} />
|
|
906
|
+
</div>
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Simple presentation components
|
|
911
|
+
// Use memo to prevent re-render if another part of the state has changed than "title"
|
|
912
|
+
const Title = memo(function Title(props: { title: string }) {
|
|
913
|
+
return <div>{props.text}</div>;
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
const Subtitle = memo(function Subtitle(props: { subtitle: string }) {
|
|
917
|
+
return <div>{props.subtitle}</div>;
|
|
918
|
+
});
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### Local Store
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
import React, { memo } from 'react';
|
|
925
|
+
import { useLocalStore } from 'state-decorator';
|
|
926
|
+
|
|
927
|
+
// Declare typings & actions as above
|
|
928
|
+
|
|
929
|
+
// Create and bind to react component
|
|
930
|
+
// Each time the store state changes, a re-render is done.
|
|
931
|
+
export function Container(props: Props) {
|
|
932
|
+
const { state, actions } = useLocalStore(config, props);
|
|
933
|
+
return (
|
|
934
|
+
<div>
|
|
935
|
+
<Title text={state.title} />
|
|
936
|
+
<Subtitle text={state.subtitle} />
|
|
937
|
+
</div>
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Simple presentation components
|
|
942
|
+
// Use memo to prevent re-render if another part of the state has changed than "title"
|
|
943
|
+
const Title = memo(function Title(props: { title: string }) {
|
|
944
|
+
return <div>{props.title}</div>;
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const Subtitle = memo(function Subtitle(props: { subtitle: string }) {
|
|
948
|
+
return <div>{props.subtitle}</div>;
|
|
949
|
+
});
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
## State slices
|
|
953
|
+
|
|
954
|
+
Using React props implies useless re-renders and optionally memoization.
|
|
955
|
+
|
|
956
|
+
To prevent this, React provides [context](https://en.reactjs.org/docs/context.html) hooks.
|
|
957
|
+
|
|
958
|
+
The problem is that if the context contains a complex state with lots of props, if **any** of these props is changing, **all** components that uses this context will be re-rendered.
|
|
959
|
+
|
|
960
|
+
To overcome this problem, the StateDecorator provides a **useStoreSlice** and **useStoreContextSlice** hook that allow to define and extracts a slice of the state and trigger a re-render of the React component only if the slice has changed.
|
|
961
|
+
|
|
962
|
+
[](https://codesandbox.io/s/slices-v6-eg471?file=/src/SliceView.tsx)
|
|
963
|
+
|
|
964
|
+
## Global Store
|
|
965
|
+
|
|
966
|
+
```typescript
|
|
967
|
+
// Declare typings & actions as above
|
|
968
|
+
|
|
969
|
+
// Create a store in a dedicated file, named GlobalStore.ts for example
|
|
970
|
+
export const store = createStore(getInitialState, userAppActions);
|
|
971
|
+
|
|
972
|
+
//----------------------
|
|
973
|
+
|
|
974
|
+
import React, { memo } from 'react';
|
|
975
|
+
import { useStoreSlice } from 'state-decorator';
|
|
976
|
+
import store from './GlobalStore';
|
|
977
|
+
|
|
978
|
+
export function Container(props: Props) {
|
|
979
|
+
return (
|
|
980
|
+
<div>
|
|
981
|
+
<Title />
|
|
982
|
+
<Subtitle />
|
|
983
|
+
</div>
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function Title() {
|
|
988
|
+
// extracts the "title" slice
|
|
989
|
+
// component is refresh only if title is changed in the store
|
|
990
|
+
const { title } = useStoreSlice(store, ['title']);
|
|
991
|
+
return <div>{title}</div>;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function Subtitle(props: { subtitle: string }) {
|
|
995
|
+
const { subtitle } = useStoreSlice(store, ['subtitle']);
|
|
996
|
+
return <div>{subtitle}</div>;
|
|
997
|
+
}
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
## Local Store
|
|
1001
|
+
|
|
1002
|
+
```typescript
|
|
1003
|
+
// ------- stores/MyStore.ts --------
|
|
1004
|
+
|
|
1005
|
+
import React from 'react';
|
|
1006
|
+
import { useLocalStore, useStoreContextSlice } from 'state-decorator';
|
|
1007
|
+
|
|
1008
|
+
// Declare typings & actions as above
|
|
1009
|
+
|
|
1010
|
+
// Create a store context
|
|
1011
|
+
|
|
1012
|
+
type StoreContextProps = StoreApi<State, Actions, Props>;
|
|
1013
|
+
|
|
1014
|
+
export const StoreContext = createContext<StoreContextProps>(null);
|
|
1015
|
+
|
|
1016
|
+
// The StoreContext will allow to access to the store
|
|
1017
|
+
// But this context is NOT refreshed if the store state is changed
|
|
1018
|
+
export function StoreContextProvider(p: { children: any; propIn: string }) {
|
|
1019
|
+
const { children, ...props } = p;
|
|
1020
|
+
const store = useLocalStore(config, props);
|
|
1021
|
+
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// In a container file ------------------------
|
|
1025
|
+
|
|
1026
|
+
import { StoreContextProvider } from 'stores/MyStore';
|
|
1027
|
+
|
|
1028
|
+
// If Container is destroyed store is destroyed
|
|
1029
|
+
export function Container(props: Props) {
|
|
1030
|
+
return (
|
|
1031
|
+
<StoreContextProvider {...props}>
|
|
1032
|
+
<SubComponent />
|
|
1033
|
+
<SubComponent2 />
|
|
1034
|
+
</StoreContextProvider>
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Component deeper in the component tree...
|
|
1039
|
+
|
|
1040
|
+
import { StoreContext } from 'stores/MyStore';
|
|
1041
|
+
|
|
1042
|
+
function Title() {
|
|
1043
|
+
// extracts the "title" slice
|
|
1044
|
+
// component is refresh only if title is changed in the store
|
|
1045
|
+
const { title } = useStoreContextSlice(StoreContext, ['title']);
|
|
1046
|
+
|
|
1047
|
+
// same as
|
|
1048
|
+
// const store = useContext(StoreContext)
|
|
1049
|
+
// const { title } = useStoreSlice(store, ['title']);
|
|
1050
|
+
|
|
1051
|
+
return <div>{title}</div>;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function Subtitle(props: { subtitle: string }) {
|
|
1055
|
+
const { subtitle } = useStoreContextSlice(StoreContext, ['subtitle']);
|
|
1056
|
+
return <div>{subtitle}</div>;
|
|
1057
|
+
}
|
|
1058
|
+
```
|
|
1059
|
+
|
|
782
1060
|
# Derived state
|
|
783
1061
|
|
|
784
1062
|
A derived state is a state that can be deduced from state and props.
|
|
@@ -802,23 +1080,21 @@ type DerivedState = {
|
|
|
802
1080
|
derivedProp: number;
|
|
803
1081
|
};
|
|
804
1082
|
|
|
805
|
-
const store = createStore<State, Actions, Props, DerivedState>(
|
|
806
|
-
() => ({ value: 0 }),
|
|
807
|
-
{
|
|
1083
|
+
const store = createStore<State, Actions, Props, DerivedState>({
|
|
1084
|
+
getInitialState: () => ({ value: 0 }),
|
|
1085
|
+
actions: {
|
|
808
1086
|
add1: ({ s }) => ({ ...s, value: s.value + 1 }),
|
|
809
1087
|
},
|
|
810
|
-
{
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
get: ({ s, p }) => s.value * p.propIn,
|
|
818
|
-
},
|
|
1088
|
+
derivedState: {
|
|
1089
|
+
derivedProp: {
|
|
1090
|
+
// get the list of dependencies to be checked to trigger the computation of the derived state
|
|
1091
|
+
// if state.value AND/OR props.prop1 changes, derivedProp is recomputed
|
|
1092
|
+
getDeps: ({ state, props }) => [state.value, props.propIn],
|
|
1093
|
+
// compute derived state from state & props (use short aliases)
|
|
1094
|
+
get: ({ s, p }) => s.value * p.propIn,
|
|
819
1095
|
},
|
|
820
|
-
}
|
|
821
|
-
);
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
822
1098
|
|
|
823
1099
|
export function App(props: Props) {
|
|
824
1100
|
const { state } = useStore(store, props);
|
|
@@ -834,6 +1110,32 @@ export function App(props: Props) {
|
|
|
834
1110
|
- In general, do **not** compute derived values directly in state, it's error prone as you can forget some places or implement different logic.
|
|
835
1111
|
- Use derived state especially if this state is shared accross several sub components
|
|
836
1112
|
|
|
1113
|
+
# Global configuration
|
|
1114
|
+
|
|
1115
|
+
Overrides configuration to set properties that will be used by all stores.
|
|
1116
|
+
|
|
1117
|
+
```typescript
|
|
1118
|
+
// all parameters are optional
|
|
1119
|
+
setGlobalConfig({
|
|
1120
|
+
// Used to clone state and props when managing optimistic conflicting actions.
|
|
1121
|
+
clone: defaultCloneFunc,
|
|
1122
|
+
// Compare to objets and returns if they are equal (slices).
|
|
1123
|
+
comparator: shallow,
|
|
1124
|
+
// Callback function to handle asychronous actions rejected promise (error reporting).
|
|
1125
|
+
asyncErrorHandler: () => {},
|
|
1126
|
+
// Tests if the error will trigger a retry of the action or will fail directly (retry promises)
|
|
1127
|
+
retryOnErrorFunction: (error: Error) => error instanceof TypeError,
|
|
1128
|
+
// Notification function on successful asynchronous action (if success message is set on action)
|
|
1129
|
+
notifySuccess: undefined,
|
|
1130
|
+
// Notification function on failed asynchronous action (if error message is set on action)
|
|
1131
|
+
notifyError: undefined,
|
|
1132
|
+
// Notification function injected in side effects action context (success and error) to notify warning
|
|
1133
|
+
notifyWarning: undefined,
|
|
1134
|
+
// Function called to return common error message if error message is not provided or error not managed in action. To override locally in action, provide a function that returns _null_.
|
|
1135
|
+
getErrorMessage: undefined,
|
|
1136
|
+
});
|
|
1137
|
+
```
|
|
1138
|
+
|
|
837
1139
|
# Debug actions
|
|
838
1140
|
|
|
839
1141
|
To debug actions, add a middleware to the store.
|
|
@@ -844,7 +1146,7 @@ To debug actions, add a middleware to the store.
|
|
|
844
1146
|
import { logEffects } from 'state-decorator/middlewares';
|
|
845
1147
|
|
|
846
1148
|
function Container(props: Props) {
|
|
847
|
-
const { state, actions } = useLocalStore(getInitialState,
|
|
1149
|
+
const { state, actions } = useLocalStore({ getInitialState, actions, middlewares: [logEffects()] });
|
|
848
1150
|
return <div />;
|
|
849
1151
|
}
|
|
850
1152
|
```
|
|
@@ -858,7 +1160,7 @@ function logger(...args: any[]) {
|
|
|
858
1160
|
|
|
859
1161
|
const logMiddleware = logEffects(logger);
|
|
860
1162
|
|
|
861
|
-
const store = createStore(getInitialState,
|
|
1163
|
+
const store = createStore({ getInitialState, actions, middlewares: [logMiddleware] });
|
|
862
1164
|
```
|
|
863
1165
|
|
|
864
1166
|
Example:
|
|
@@ -874,9 +1176,7 @@ Example:
|
|
|
874
1176
|
import { logDetailedEffects } from 'state-decorator/middlewares';
|
|
875
1177
|
|
|
876
1178
|
function Container(props: Props) {
|
|
877
|
-
const { state, actions } = useLocalStore(getInitialState,
|
|
878
|
-
logDetailedEffects(),
|
|
879
|
-
]);
|
|
1179
|
+
const { state, actions } = useLocalStore({ getInitialState, actions, middlewares: [logDetailedEffects()] });
|
|
880
1180
|
return <div />;
|
|
881
1181
|
}
|
|
882
1182
|
```
|
|
@@ -905,7 +1205,7 @@ You can connect any StateDecorator store to [Chrome Redux devtools extension](ht
|
|
|
905
1205
|
import { devtools } from 'state-decorator/middlewares';
|
|
906
1206
|
|
|
907
1207
|
function Container(props: Props) {
|
|
908
|
-
const { state, actions } = useLocalStore(getInitialState,
|
|
1208
|
+
const { state, actions } = useLocalStore({ getInitialState, actions, middlewares: [devtools()] });
|
|
909
1209
|
return <div />;
|
|
910
1210
|
}
|
|
911
1211
|
```
|
|
@@ -917,11 +1217,11 @@ function Container(props: Props) {
|
|
|
917
1217
|
- a new store is created each time state or props are set.
|
|
918
1218
|
- it allows to share mock store across tests.
|
|
919
1219
|
- The API is chainable: functions are returning a mock store, excepting test functions.
|
|
920
|
-
- On a mock store, we can test the store internals after initialiation, after inbound props have changed, after actions are called.
|
|
1220
|
+
- On a mock store, we can test the store internals after initialiation, after inbound props have changed, after actions are called. These test function do not change the internal state of the mock store.
|
|
921
1221
|
|
|
922
1222
|
## Getting started
|
|
923
1223
|
|
|
924
|
-
1. Create and setup a mock store (_createMockFromStore_)
|
|
1224
|
+
1. Create and setup a mock store (_createMockFromStore_, _createMockStore_)
|
|
925
1225
|
2. Test store after initialization (_onInit_)
|
|
926
1226
|
3. Test store after props changes (_onPropsChange_)
|
|
927
1227
|
4. Test each action:
|
|
@@ -1419,7 +1719,65 @@ TypeError: Cannot set property 'crash' of null
|
|
|
1419
1719
|
|
|
1420
1720
|
# Migration
|
|
1421
1721
|
|
|
1422
|
-
##
|
|
1722
|
+
## V6 to V7
|
|
1723
|
+
|
|
1724
|
+
### Step 1: V6 compatibility layer
|
|
1725
|
+
|
|
1726
|
+
```diff
|
|
1727
|
+
-import useLocalStore from 'state-decorator';
|
|
1728
|
+
-import { useLocalStore } from 'state-decorator';
|
|
1729
|
+
|
|
1730
|
+
+import { useLocalStoreV6 } from 'state-decorator';
|
|
1731
|
+
```
|
|
1732
|
+
|
|
1733
|
+
### Step 2: Migrate code
|
|
1734
|
+
|
|
1735
|
+
#### Config
|
|
1736
|
+
|
|
1737
|
+
```diff
|
|
1738
|
+
- const getInitialState: (p:Props) => ({myProp: ''});
|
|
1739
|
+
-
|
|
1740
|
+
- const actions: StoreActions<State, Actions, Props> = {
|
|
1741
|
+
- myAction: ({s, args: [v]}) => ({ ...s, myProp: v })
|
|
1742
|
+
-}
|
|
1743
|
+
-
|
|
1744
|
+
- const options: StoreOptions<State, Action, Props> = {
|
|
1745
|
+
- onMount: () => {},
|
|
1746
|
+
- onPropsChange: {
|
|
1747
|
+
- getDeps: () => [],
|
|
1748
|
+
- effects: ({s, p}) => ({ ...s, myProp: p.prop }),
|
|
1749
|
+
- }
|
|
1750
|
+
- };
|
|
1751
|
+
-
|
|
1752
|
+
- function MyComponent(p:Props) {
|
|
1753
|
+
- const {state, actions} = useLocalStore(getInitialState, actions, props, options, [logEffects()]);
|
|
1754
|
+
- return <div>{myProp}</div>;
|
|
1755
|
+
- }
|
|
1756
|
+
|
|
1757
|
+
+ const config:StoreConfig<State, Actions, Props> = {
|
|
1758
|
+
+ getInitialState: () => ({ myProp: '' }),
|
|
1759
|
+
+ actions: {
|
|
1760
|
+
+ myAction: ({args: [v]}) => ({ myProp: v})
|
|
1761
|
+
+ // or
|
|
1762
|
+
+ myAction: setArgIn('myProp'),
|
|
1763
|
+
+ },
|
|
1764
|
+
+ onMount: () => {},
|
|
1765
|
+
+ onPropsChange: {
|
|
1766
|
+
+ getDeps: () => [],
|
|
1767
|
+
+ effects: ({ p }) => ({ myProp: p.prop })
|
|
1768
|
+
+ },
|
|
1769
|
+
+ middlewares: [logEffects()],
|
|
1770
|
+
+ }
|
|
1771
|
+
+
|
|
1772
|
+
+ function MyComponent(p:Props) {
|
|
1773
|
+
+ const {state, actions} = useLocalStore(config);
|
|
1774
|
+
+ return <div>{myProp}</div>;
|
|
1775
|
+
+ }
|
|
1776
|
+
```
|
|
1777
|
+
|
|
1778
|
+
## V5 to V6
|
|
1779
|
+
|
|
1780
|
+
### Step 1: v5 compatibility layer
|
|
1423
1781
|
|
|
1424
1782
|
To use the new engine while still using v5 API, juste import the v5 compatibility layer.
|
|
1425
1783
|
You can new store in new code.
|
|
@@ -1440,26 +1798,26 @@ setGlobalConfig({
|
|
|
1440
1798
|
});
|
|
1441
1799
|
```
|
|
1442
1800
|
|
|
1443
|
-
|
|
1801
|
+
### Step 2: Migrate code but keep v5 tests
|
|
1444
1802
|
|
|
1445
|
-
|
|
1803
|
+
#### Code
|
|
1446
1804
|
|
|
1447
|
-
|
|
1805
|
+
##### Types
|
|
1448
1806
|
|
|
1449
1807
|
| Before | After |
|
|
1450
1808
|
| --------------------- | ------------ |
|
|
1451
1809
|
| StateDecoratorActions | StoreActions |
|
|
1452
1810
|
| StateDecoratorOptions | StoreOptions |
|
|
1453
1811
|
|
|
1454
|
-
|
|
1812
|
+
##### Optimistic actions
|
|
1455
1813
|
|
|
1456
1814
|
If you are using optimistic effects, make sure to set the **optimisticActions** middleware to your stores, see [Optimistic actions](#optimistic-actions).
|
|
1457
1815
|
|
|
1458
|
-
|
|
1816
|
+
#### Hooks
|
|
1459
1817
|
|
|
1460
1818
|
Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
1461
1819
|
|
|
1462
|
-
|
|
1820
|
+
#### Actions
|
|
1463
1821
|
|
|
1464
1822
|
- rename action properties
|
|
1465
1823
|
|
|
@@ -1500,9 +1858,9 @@ Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
|
1500
1858
|
};
|
|
1501
1859
|
```
|
|
1502
1860
|
|
|
1503
|
-
|
|
1861
|
+
#### Options
|
|
1504
1862
|
|
|
1505
|
-
|
|
1863
|
+
##### onMount
|
|
1506
1864
|
|
|
1507
1865
|
```diff
|
|
1508
1866
|
- const options: StateDecoratorOptions<S, A, P> = {
|
|
@@ -1514,7 +1872,7 @@ Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
|
1514
1872
|
+} ;
|
|
1515
1873
|
```
|
|
1516
1874
|
|
|
1517
|
-
|
|
1875
|
+
##### logEnabled
|
|
1518
1876
|
|
|
1519
1877
|
```diff
|
|
1520
1878
|
- function Container(props: Props) {
|
|
@@ -1530,7 +1888,7 @@ Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
|
1530
1888
|
+ }
|
|
1531
1889
|
```
|
|
1532
1890
|
|
|
1533
|
-
|
|
1891
|
+
##### Props changes
|
|
1534
1892
|
|
|
1535
1893
|
```diff
|
|
1536
1894
|
- const options: StateDecoratorOptions<S, A, P> = {
|
|
@@ -1552,7 +1910,7 @@ Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
|
1552
1910
|
+} ;
|
|
1553
1911
|
```
|
|
1554
1912
|
|
|
1555
|
-
|
|
1913
|
+
#### Reuse v5 tests with v6 actions
|
|
1556
1914
|
|
|
1557
1915
|
```diff
|
|
1558
1916
|
- import { testSyncAction, testAsyncAction } from 'state-decorator';
|
|
@@ -1560,7 +1918,7 @@ Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
|
1560
1918
|
+ import { testV6SyncAction, testV6AsyncAction } from 'state-decorator/v5_test';
|
|
1561
1919
|
```
|
|
1562
1920
|
|
|
1563
|
-
|
|
1921
|
+
### Step 3 (optional): Migrate tests
|
|
1564
1922
|
|
|
1565
1923
|
🎊 Congratulations! 🎊
|
|
1566
1924
|
|
|
@@ -1608,34 +1966,27 @@ You can add this snippet to quickly create a stateful functional component using
|
|
|
1608
1966
|
"import React from 'react';",
|
|
1609
1967
|
"import { useStore, StoreActions, LoadingProps } from 'state-decorator';",
|
|
1610
1968
|
"",
|
|
1611
|
-
"export type
|
|
1969
|
+
"export type Props = {};",
|
|
1612
1970
|
"",
|
|
1613
|
-
"export type
|
|
1971
|
+
"export type State = {};",
|
|
1614
1972
|
"",
|
|
1615
|
-
"export type
|
|
1616
|
-
"",
|
|
1617
|
-
"type Props = $1Props;",
|
|
1618
|
-
"type State = $1State;",
|
|
1619
|
-
"type Actions = $1Actions;",
|
|
1973
|
+
"export type Actions = {};",
|
|
1620
1974
|
"",
|
|
1621
1975
|
"type ViewProps = Props & Actions & State & Pick<LoadingProps<Actions>, 'loadingMap'>;",
|
|
1622
1976
|
"",
|
|
1623
|
-
"export
|
|
1624
|
-
"
|
|
1977
|
+
"export const storeConfig: StoreConfig<State, Actions, Props> = {",
|
|
1978
|
+
" getInitialState: (p:$1Props) => ({",
|
|
1979
|
+
" }),",
|
|
1980
|
+
" actions: {",
|
|
1981
|
+
" }",
|
|
1625
1982
|
"};",
|
|
1626
1983
|
"",
|
|
1627
1984
|
"export function $1View(p: ViewProps) {",
|
|
1628
1985
|
" return (<div>[$1View]</div>);",
|
|
1629
1986
|
"}",
|
|
1630
1987
|
"",
|
|
1631
|
-
"export const actions$1: StoreActions<State, Actions, Props> = {};",
|
|
1632
|
-
"",
|
|
1633
|
-
"export function onMount(actions:$1Actions) {}",
|
|
1634
|
-
"",
|
|
1635
|
-
"const store = createStore(getInitialState, actions$1, { onMount });",
|
|
1636
|
-
"",
|
|
1637
1988
|
"export default React.memo(function $1(p: $1Props) {",
|
|
1638
|
-
" const { state:s, actions:a, loadingMap } =
|
|
1989
|
+
" const { state:s, actions:a, loadingMap } = useLocalStore(config, p);",
|
|
1639
1990
|
" return <$1View {...p} {...s} {...a} loadingMap={loadingMap} />;",
|
|
1640
1991
|
"});"
|
|
1641
1992
|
],
|