state-decorator 7.0.0-beta.0 → 7.0.0-beta.10-dev
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 +384 -192
- package/es/helpers.d.ts +66 -0
- package/es/helpers.js +163 -0
- package/{dist/v5_test.js.map → es/helpers.js.map} +1 -1
- package/es/impl.d.ts +19 -5
- package/es/impl.js +963 -1
- package/es/impl.js.map +1 -1
- package/es/index.d.ts +34 -36
- package/es/index.js +1013 -1
- package/es/index.js.map +1 -1
- package/es/middlewares.js +519 -1
- package/es/middlewares.js.map +1 -1
- package/es/test.d.ts +6 -4
- package/es/test.js +349 -1
- package/es/test.js.map +1 -1
- package/es/types.d.ts +32 -3
- package/es/v5.js +301 -1
- package/es/v5.js.map +1 -1
- package/es/v5_test.js +392 -1
- package/es/v5_test.js.map +1 -1
- package/es/v6.d.ts +18 -0
- package/es/v6.js +98 -0
- package/es/v6.js.map +1 -0
- package/helpers.d.ts +66 -0
- package/helpers.js +175 -0
- package/{dist/test.js.map → helpers.js.map} +1 -1
- package/impl.d.ts +19 -5
- package/impl.js +996 -1
- package/impl.js.map +1 -1
- package/index.d.ts +34 -36
- package/index.js +1044 -1
- package/index.js.map +1 -1
- package/middlewares.js +526 -1
- package/middlewares.js.map +1 -1
- package/package.json +5 -4
- package/test.d.ts +6 -4
- package/test.js +358 -1
- package/test.js.map +1 -1
- package/types.d.ts +32 -3
- package/v5.js +322 -1
- package/v5.js.map +1 -1
- package/v5_test.js +416 -1
- package/v5_test.js.map +1 -1
- package/v6.d.ts +18 -0
- package/v6.js +123 -0
- package/v6.js.map +1 -0
- package/dist/impl.d.ts +0 -195
- package/dist/impl.js +0 -2
- package/dist/impl.js.map +0 -1
- package/dist/index.d.ts +0 -168
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/middlewares.d.ts +0 -26
- package/dist/middlewares.js +0 -2
- package/dist/middlewares.js.map +0 -1
- package/dist/shallow.d.ts +0 -15
- package/dist/test.d.ts +0 -134
- package/dist/test.js +0 -2
- package/dist/types.d.ts +0 -352
- package/dist/v5.d.ts +0 -58
- package/dist/v5.js +0 -2
- package/dist/v5.js.map +0 -1
- package/dist/v5_test.d.ts +0 -70
- package/dist/v5_test.js +0 -2
- package/dist/v5_types.d.ts +0 -227
package/README.md
CHANGED
|
@@ -19,10 +19,13 @@ The StateDecorator is a set of Reacts hook that manages a complex component stat
|
|
|
19
19
|
|
|
20
20
|
# V7: Partial state and new hooks
|
|
21
21
|
|
|
22
|
+
- React 17 (upcoming V8 for React 18)
|
|
22
23
|
- All effects can now return a partial state that will be merged with current state.
|
|
23
24
|
- More documentation on global and local store use cases.
|
|
24
25
|
- **useLocalStore** and **useStore** now have a parameter to specify if the React component must re-rerender after a state change.
|
|
25
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
|
|
26
29
|
- Removed useBindStore hook.
|
|
27
30
|
|
|
28
31
|
# 🏎️ V6: The Store
|
|
@@ -55,8 +58,7 @@ yarn add state-decorator
|
|
|
55
58
|
### Example
|
|
56
59
|
|
|
57
60
|
```typescript
|
|
58
|
-
import
|
|
59
|
-
import { useLocalStore, StoreActions } from 'state-decorator';
|
|
61
|
+
import { useLocalStore, StoreConfig } from "state-decorator";
|
|
60
62
|
|
|
61
63
|
// Typings
|
|
62
64
|
|
|
@@ -81,45 +83,48 @@ type Props = {
|
|
|
81
83
|
};
|
|
82
84
|
|
|
83
85
|
// Initial state & actions
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
},
|
|
101
104
|
},
|
|
102
|
-
},
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
},
|
|
117
121
|
},
|
|
118
122
|
};
|
|
119
123
|
|
|
124
|
+
|
|
120
125
|
// Bind to react component and use state/actions
|
|
121
126
|
export function App(props: Props) => {
|
|
122
|
-
const { state, actions } = useLocalStore(
|
|
127
|
+
const { state, actions } = useLocalStore(config);
|
|
123
128
|
return (
|
|
124
129
|
<div>
|
|
125
130
|
{state.user && (
|
|
@@ -157,12 +162,13 @@ export function App(props: Props) => {
|
|
|
157
162
|
The initial state is build using a function provided to the store.
|
|
158
163
|
|
|
159
164
|
```typescript
|
|
160
|
-
|
|
161
|
-
counter: 0,
|
|
162
|
-
}
|
|
165
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
166
|
+
getInitialState: (p) => ({ counter: 0 }),
|
|
167
|
+
actions: {},
|
|
168
|
+
};
|
|
163
169
|
|
|
164
170
|
export const CounterContainer = () => {
|
|
165
|
-
const { state, actions } = useLocalStore(
|
|
171
|
+
const { state, actions } = useLocalStore(config);
|
|
166
172
|
return <CounterView {...state} {...actions} />;
|
|
167
173
|
};
|
|
168
174
|
```
|
|
@@ -182,6 +188,7 @@ The action context contain all the data needed to implement the actions. It cont
|
|
|
182
188
|
| derived | ds | everywhere | The derived state (from props/state) |
|
|
183
189
|
| promiseId | | asynchronous actions callbacks | Parallel asynchronous actions promise identifier |
|
|
184
190
|
| notifyWarning | | side effects | Injected function if set at store or global level |
|
|
191
|
+
| clearError | | side effects | Clear an error from ErrorMap |
|
|
185
192
|
| indices | | onPropsChange | Indices in the getDeps array of changed props |
|
|
186
193
|
| isInit | | onPropsChange | Whether the onPropsCHange is executed during init phase |
|
|
187
194
|
|
|
@@ -202,8 +209,10 @@ action: (ctx) => ctx.state, // changes to be merged to current state
|
|
|
202
209
|
Example:
|
|
203
210
|
|
|
204
211
|
```typescript
|
|
205
|
-
const
|
|
206
|
-
|
|
212
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
213
|
+
actions: {
|
|
214
|
+
setText: ({ args: [newText] }) => ({ text: newText }),
|
|
215
|
+
},
|
|
207
216
|
};
|
|
208
217
|
```
|
|
209
218
|
|
|
@@ -226,14 +235,16 @@ action: {
|
|
|
226
235
|
Example:
|
|
227
236
|
|
|
228
237
|
```typescript
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
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
|
+
},
|
|
237
248
|
},
|
|
238
249
|
},
|
|
239
250
|
};
|
|
@@ -275,14 +286,16 @@ action: {
|
|
|
275
286
|
Example:
|
|
276
287
|
|
|
277
288
|
```typescript
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
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',
|
|
284
298
|
},
|
|
285
|
-
getErrorMessage: () => 'Cannot load list',
|
|
286
299
|
},
|
|
287
300
|
};
|
|
288
301
|
```
|
|
@@ -380,11 +393,13 @@ const store = useLocalStore(getInitialState, actionsImpl, props, {
|
|
|
380
393
|
|
|
381
394
|
```typescript
|
|
382
395
|
// action implementation
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
},
|
|
388
403
|
},
|
|
389
404
|
};
|
|
390
405
|
```
|
|
@@ -445,6 +460,8 @@ if (errorMap.action1 instanceof MyCustomError){
|
|
|
445
460
|
|
|
446
461
|
Recipe: When only using the **errorMap** to manage errors (ie. there's no effects nor message and so on), the error is considered _not managed_ resulting in a rejected promise and global **asyncErrorHandler** called with _managed_ flag set to false. In that case, set **isErrorManaged** to `true` to specify that the error is _managed_ externally.
|
|
447
462
|
|
|
463
|
+
The error map is updated when the action is called again, or call **clearError** in a side effect.
|
|
464
|
+
|
|
448
465
|
## <a name="ConflictingActions"></a>Conflicting actions
|
|
449
466
|
|
|
450
467
|
The StateDecorator is managing the asynchronous action calls one at a time (default) or in parallel.
|
|
@@ -499,10 +516,20 @@ setGlobalConfig({
|
|
|
499
516
|
On a specific store:
|
|
500
517
|
|
|
501
518
|
```typescript
|
|
502
|
-
import { createStore } from 'state-decorator';
|
|
519
|
+
import { createStore, StoreConfig } from 'state-decorator';
|
|
503
520
|
import { optimisticActions } from 'state-decorator/middlewares';
|
|
504
521
|
|
|
505
|
-
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);
|
|
506
533
|
```
|
|
507
534
|
|
|
508
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).
|
|
@@ -520,11 +547,13 @@ The undo strategy is the following:
|
|
|
520
547
|
Example:
|
|
521
548
|
|
|
522
549
|
```typescript
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
+
},
|
|
528
557
|
},
|
|
529
558
|
};
|
|
530
559
|
```
|
|
@@ -548,119 +577,167 @@ Ongoing asynchronous actions can be aborted using, under the hood, the [AbortCon
|
|
|
548
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.
|
|
549
578
|
|
|
550
579
|
```typescript
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
+
},
|
|
571
602
|
},
|
|
572
603
|
},
|
|
573
604
|
};
|
|
574
605
|
```
|
|
575
606
|
|
|
607
|
+
Note: ongoing abortable actions will be automatically aborted on store destruction (during React component unmount for example). The list of aborted actions is available in the **onUnmount** callback in the store's options.
|
|
608
|
+
|
|
576
609
|
[](https://codesandbox.io/s/abort-v6-uvnl7)
|
|
577
610
|
|
|
578
|
-
|
|
611
|
+
## Helper effect functions
|
|
579
612
|
|
|
580
|
-
|
|
613
|
+
For common / simple use cases, some helpers functions are provided to write basic action effects and improve actions readability.
|
|
614
|
+
|
|
615
|
+
Example
|
|
581
616
|
|
|
582
617
|
```typescript
|
|
583
|
-
import {
|
|
618
|
+
import { setArgIn } from 'state-decorator/helpers';
|
|
584
619
|
|
|
585
620
|
type State = {
|
|
586
|
-
|
|
621
|
+
myProp: string;
|
|
587
622
|
};
|
|
588
623
|
|
|
589
624
|
type Actions = {
|
|
590
|
-
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
type Props = {
|
|
594
|
-
propIn: number;
|
|
625
|
+
setMyProp: (v: string) => void;
|
|
595
626
|
};
|
|
596
627
|
|
|
597
|
-
|
|
598
|
-
|
|
628
|
+
const actions: StoreActions<State, Actions> = {
|
|
629
|
+
setMyProp: setArgIn('myProp'),
|
|
630
|
+
// same as
|
|
631
|
+
// setMyProp: ({ args: [v] }) => ({ myProp: v }),
|
|
599
632
|
};
|
|
633
|
+
```
|
|
600
634
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
// compute derived state from state & props (use short aliases)
|
|
613
|
-
get: ({ s, p }) => s.value * p.propIn,
|
|
614
|
-
},
|
|
615
|
-
},
|
|
616
|
-
}
|
|
617
|
-
);
|
|
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> |
|
|
618
646
|
|
|
619
|
-
|
|
620
|
-
const { state } = useStore(store, props);
|
|
621
|
-
// state contains the store state and derived state
|
|
622
|
-
return <div>{state.derivedProp}</div>;
|
|
623
|
-
}
|
|
624
|
-
```
|
|
647
|
+
## Call actions on mount
|
|
625
648
|
|
|
626
|
-
|
|
649
|
+
When store is created (for example whe React component in mounted), the **onMount** option is called.
|
|
627
650
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
- In general, do **not** compute derived values directly in state, it's error prone as you can forget some places or implement different logic.
|
|
631
|
-
- Use derived state especially if this state is shared accross several sub components
|
|
651
|
+
```typescript
|
|
652
|
+
import { StoreConfig } from 'state-decorator';
|
|
632
653
|
|
|
633
|
-
|
|
654
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
655
|
+
getInitialState,
|
|
656
|
+
actions,
|
|
657
|
+
onMount: ({ p }) => {
|
|
658
|
+
p.loadParentList();
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
```
|
|
634
662
|
|
|
635
|
-
|
|
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.
|
|
636
666
|
|
|
637
667
|
```typescript
|
|
638
|
-
import {
|
|
668
|
+
import { StoreConfig } from 'state-decorator';
|
|
639
669
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
670
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
671
|
+
getInitialState,
|
|
672
|
+
actions,
|
|
673
|
+
onMountDeferred: ({ p }) => {
|
|
674
|
+
p.loadParentList();
|
|
644
675
|
},
|
|
645
|
-
}
|
|
676
|
+
};
|
|
677
|
+
```
|
|
646
678
|
|
|
647
|
-
|
|
679
|
+
If an initial action is launched each time a property changes, consider using the **onMount** flag of the **onPropsChange** entry.
|
|
680
|
+
|
|
681
|
+
## Call actions on store destruction / unmount
|
|
682
|
+
|
|
683
|
+
When the store is destroyed (for example on React component unmounted), the **onUnmount** option is called.
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
import { StoreConfig } from 'state-decorator';
|
|
687
|
+
|
|
688
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
689
|
+
getInitialState,
|
|
690
|
+
actions,
|
|
691
|
+
onUnmount: ({ s, abortedActions }) => {
|
|
692
|
+
// ex: store s in local storage
|
|
693
|
+
// ex: clean cache of aborted actions
|
|
694
|
+
},
|
|
695
|
+
};
|
|
648
696
|
```
|
|
649
697
|
|
|
698
|
+
## State sharing and slices
|
|
699
|
+
|
|
700
|
+
- Declare a store and bind it to a component using _useStore_ or _useBindStore_ hooks.
|
|
701
|
+
- Deeper in the tree component, use _useStoreSlice_ to get a store slice.
|
|
702
|
+
- Component using slices will be refreshed only if their store slice changes.
|
|
703
|
+
|
|
650
704
|
```typescript
|
|
651
|
-
import
|
|
705
|
+
import React from 'react';
|
|
706
|
+
import { useStore, useStoreSlice, StoreActions } from 'state-decorator';
|
|
652
707
|
|
|
653
|
-
//
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
708
|
+
// Declare typings & actions as above
|
|
709
|
+
|
|
710
|
+
// Create a store
|
|
711
|
+
|
|
712
|
+
export const store = createStore(config);
|
|
713
|
+
|
|
714
|
+
// Bind to react component
|
|
715
|
+
|
|
716
|
+
export function Container(prop: Props) {
|
|
717
|
+
const { state, actions } = useStore(store, props);
|
|
718
|
+
|
|
719
|
+
// or
|
|
720
|
+
// useBindStore(store, props);
|
|
721
|
+
// if you are not interested in getting the state here
|
|
722
|
+
|
|
723
|
+
return <div />;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Components deeper in the component tree will be refreshed if, and only if,
|
|
727
|
+
// slice is changed (here: text property)
|
|
728
|
+
|
|
729
|
+
export function SubComponent2() {
|
|
730
|
+
const s = useStoreSlice(store, ['text']);
|
|
731
|
+
return <div>{s.text}</div>;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export function SubComponent() {
|
|
735
|
+
const s = useStoreSlice(store, (s) => ({ text: s.text }));
|
|
736
|
+
return <div>{s.text}</div>;
|
|
660
737
|
}
|
|
661
738
|
```
|
|
662
739
|
|
|
663
|
-
|
|
740
|
+
[](https://codesandbox.io/s/slices-v6-eg471?file=/src/SliceView.tsx)
|
|
664
741
|
|
|
665
742
|
# Update store when props change
|
|
666
743
|
|
|
@@ -669,7 +746,9 @@ If an initial action is launched each time a property changes, consider using th
|
|
|
669
746
|
```typescript
|
|
670
747
|
import { createStore } from 'state-decorator';
|
|
671
748
|
|
|
672
|
-
const
|
|
749
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
750
|
+
getInitialState,
|
|
751
|
+
actions,
|
|
673
752
|
onPropsChange: {
|
|
674
753
|
// list of depedencies that should trigger effects & side effects if changed
|
|
675
754
|
getDeps: (p) => [p.id],
|
|
@@ -682,7 +761,7 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
682
761
|
// whether apply effects & side effects on mount
|
|
683
762
|
onMount: true,
|
|
684
763
|
},
|
|
685
|
-
}
|
|
764
|
+
};
|
|
686
765
|
```
|
|
687
766
|
|
|
688
767
|
## Multiple props change configuration
|
|
@@ -694,9 +773,11 @@ The store can manage several props change configurations to have a better granul
|
|
|
694
773
|
- Side effects are invoked after all effects using computed state
|
|
695
774
|
|
|
696
775
|
```typescript
|
|
697
|
-
import {
|
|
776
|
+
import { StoreConfig } from 'state-decorator';
|
|
698
777
|
|
|
699
|
-
const
|
|
778
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
779
|
+
getInitialState,
|
|
780
|
+
actions,
|
|
700
781
|
onPropsChange: [
|
|
701
782
|
{
|
|
702
783
|
getDeps: (p) => [p.id],
|
|
@@ -716,7 +797,7 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
716
797
|
},
|
|
717
798
|
},
|
|
718
799
|
],
|
|
719
|
-
}
|
|
800
|
+
};
|
|
720
801
|
```
|
|
721
802
|
|
|
722
803
|
## OnMount
|
|
@@ -726,7 +807,11 @@ If the onMount flag is set on the props change configuration, the effects and si
|
|
|
726
807
|
It allows to trigger same actions at creation time and when the dependencies props are changed.
|
|
727
808
|
|
|
728
809
|
```typescript
|
|
729
|
-
|
|
810
|
+
import { StoreConfig } from 'state-decorator';
|
|
811
|
+
|
|
812
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
813
|
+
getInitialState,
|
|
814
|
+
actions,
|
|
730
815
|
onMount: ({ p }) => {
|
|
731
816
|
a.loadUser(p.userId);
|
|
732
817
|
},
|
|
@@ -739,11 +824,15 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
739
824
|
},
|
|
740
825
|
},
|
|
741
826
|
],
|
|
742
|
-
}
|
|
827
|
+
};
|
|
743
828
|
|
|
744
829
|
// SAME AS
|
|
745
830
|
|
|
746
|
-
|
|
831
|
+
import { StoreConfig } from 'state-decorator';
|
|
832
|
+
|
|
833
|
+
const config: StoreConfig<State, Actions, Props> = {
|
|
834
|
+
getInitialState,
|
|
835
|
+
actions,
|
|
747
836
|
onPropsChange: [
|
|
748
837
|
{
|
|
749
838
|
getDeps: (p) => [p.userId],
|
|
@@ -754,7 +843,7 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
754
843
|
onMount: true,
|
|
755
844
|
},
|
|
756
845
|
],
|
|
757
|
-
}
|
|
846
|
+
};
|
|
758
847
|
```
|
|
759
848
|
|
|
760
849
|
**Caution**: the effects are executed too. Use the isInit parameter is passed to the context to apply or not state change.
|
|
@@ -840,7 +929,7 @@ import { useLocalStore } from 'state-decorator';
|
|
|
840
929
|
// Create and bind to react component
|
|
841
930
|
// Each time the store state changes, a re-render is done.
|
|
842
931
|
export function Container(props: Props) {
|
|
843
|
-
const { state, actions } = useLocalStore(
|
|
932
|
+
const { state, actions } = useLocalStore(config, props);
|
|
844
933
|
return (
|
|
845
934
|
<div>
|
|
846
935
|
<Title text={state.title} />
|
|
@@ -928,7 +1017,7 @@ export const StoreContext = createContext<StoreContextProps>(null);
|
|
|
928
1017
|
// But this context is NOT refreshed if the store state is changed
|
|
929
1018
|
export function StoreContextProvider(p: { children: any; propIn: string }) {
|
|
930
1019
|
const { children, ...props } = p;
|
|
931
|
-
const store = useLocalStore(
|
|
1020
|
+
const store = useLocalStore(config, props);
|
|
932
1021
|
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
|
|
933
1022
|
}
|
|
934
1023
|
|
|
@@ -968,6 +1057,59 @@ function Subtitle(props: { subtitle: string }) {
|
|
|
968
1057
|
}
|
|
969
1058
|
```
|
|
970
1059
|
|
|
1060
|
+
# Derived state
|
|
1061
|
+
|
|
1062
|
+
A derived state is a state that can be deduced from state and props.
|
|
1063
|
+
|
|
1064
|
+
```typescript
|
|
1065
|
+
import { createStore } from 'state-decorator';
|
|
1066
|
+
|
|
1067
|
+
type State = {
|
|
1068
|
+
value: number;
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
type Actions = {
|
|
1072
|
+
add1: () => void;
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
type Props = {
|
|
1076
|
+
propIn: number;
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
type DerivedState = {
|
|
1080
|
+
derivedProp: number;
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const store = createStore<State, Actions, Props, DerivedState>({
|
|
1084
|
+
getInitialState: () => ({ value: 0 }),
|
|
1085
|
+
actions: {
|
|
1086
|
+
add1: ({ s }) => ({ ...s, value: s.value + 1 }),
|
|
1087
|
+
},
|
|
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,
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
export function App(props: Props) {
|
|
1100
|
+
const { state } = useStore(store, props);
|
|
1101
|
+
// state contains the store state and derived state
|
|
1102
|
+
return <div>{state.derivedProp}</div>;
|
|
1103
|
+
}
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
## Recipes
|
|
1107
|
+
|
|
1108
|
+
- Derived state is the same as using _useMemo_ in sub component.
|
|
1109
|
+
- If the derived state is needed in one component only, it may prove better to use a _useMemo_ in this component to save memory when the component is unmounted and store not.
|
|
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.
|
|
1111
|
+
- Use derived state especially if this state is shared accross several sub components
|
|
1112
|
+
|
|
971
1113
|
# Global configuration
|
|
972
1114
|
|
|
973
1115
|
Overrides configuration to set properties that will be used by all stores.
|
|
@@ -1004,7 +1146,7 @@ To debug actions, add a middleware to the store.
|
|
|
1004
1146
|
import { logEffects } from 'state-decorator/middlewares';
|
|
1005
1147
|
|
|
1006
1148
|
function Container(props: Props) {
|
|
1007
|
-
const { state, actions } = useLocalStore(getInitialState,
|
|
1149
|
+
const { state, actions } = useLocalStore({ getInitialState, actions, middlewares: [logEffects()] });
|
|
1008
1150
|
return <div />;
|
|
1009
1151
|
}
|
|
1010
1152
|
```
|
|
@@ -1018,14 +1160,14 @@ function logger(...args: any[]) {
|
|
|
1018
1160
|
|
|
1019
1161
|
const logMiddleware = logEffects(logger);
|
|
1020
1162
|
|
|
1021
|
-
const store = createStore(getInitialState,
|
|
1163
|
+
const store = createStore({ getInitialState, actions, middlewares: [logMiddleware] });
|
|
1022
1164
|
```
|
|
1023
1165
|
|
|
1024
1166
|
Example:
|
|
1025
1167
|
|
|
1026
1168
|
```
|
|
1027
1169
|
[StateDecorator sample] loadList effects {list: Array(10), prop1: "", prop2: "", prop3: ""} loading: false
|
|
1028
|
-
|
|
1170
|
+
[StateDecorator sample] setProp1 effects {list: Array(10), prop1: "92", prop2: "", prop3: ""}
|
|
1029
1171
|
```
|
|
1030
1172
|
|
|
1031
1173
|
## Detailed Logger
|
|
@@ -1034,9 +1176,7 @@ middlewares.ts:36 [StateDecorator sample] setProp1 effects {list: Array(10), pro
|
|
|
1034
1176
|
import { logDetailedEffects } from 'state-decorator/middlewares';
|
|
1035
1177
|
|
|
1036
1178
|
function Container(props: Props) {
|
|
1037
|
-
const { state, actions } = useLocalStore(getInitialState,
|
|
1038
|
-
logDetailedEffects(),
|
|
1039
|
-
]);
|
|
1179
|
+
const { state, actions } = useLocalStore({ getInitialState, actions, middlewares: [logDetailedEffects()] });
|
|
1040
1180
|
return <div />;
|
|
1041
1181
|
}
|
|
1042
1182
|
```
|
|
@@ -1065,7 +1205,7 @@ You can connect any StateDecorator store to [Chrome Redux devtools extension](ht
|
|
|
1065
1205
|
import { devtools } from 'state-decorator/middlewares';
|
|
1066
1206
|
|
|
1067
1207
|
function Container(props: Props) {
|
|
1068
|
-
const { state, actions } = useLocalStore(getInitialState,
|
|
1208
|
+
const { state, actions } = useLocalStore({ getInitialState, actions, middlewares: [devtools()] });
|
|
1069
1209
|
return <div />;
|
|
1070
1210
|
}
|
|
1071
1211
|
```
|
|
@@ -1107,7 +1247,8 @@ describe('Todo', () => {
|
|
|
1107
1247
|
// this store can be shared accross tests because it is immutable
|
|
1108
1248
|
const store = createMockFromStore(todoStore)
|
|
1109
1249
|
// or
|
|
1110
|
-
// const store = createMockStore(
|
|
1250
|
+
// const store = createMockStore(storeConfig, props)
|
|
1251
|
+
|
|
1111
1252
|
// initial state / props can be set in full form or partial form
|
|
1112
1253
|
.setPartialState({
|
|
1113
1254
|
idCount: 3,
|
|
@@ -1579,7 +1720,65 @@ TypeError: Cannot set property 'crash' of null
|
|
|
1579
1720
|
|
|
1580
1721
|
# Migration
|
|
1581
1722
|
|
|
1582
|
-
##
|
|
1723
|
+
## V6 to V7
|
|
1724
|
+
|
|
1725
|
+
### Step 1: V6 compatibility layer
|
|
1726
|
+
|
|
1727
|
+
```diff
|
|
1728
|
+
-import useLocalStore from 'state-decorator';
|
|
1729
|
+
-import { useLocalStore } from 'state-decorator';
|
|
1730
|
+
|
|
1731
|
+
+import { useLocalStoreV6 } from 'state-decorator';
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1734
|
+
### Step 2: Migrate code
|
|
1735
|
+
|
|
1736
|
+
#### Config
|
|
1737
|
+
|
|
1738
|
+
```diff
|
|
1739
|
+
- const getInitialState: (p:Props) => ({myProp: ''});
|
|
1740
|
+
-
|
|
1741
|
+
- const actions: StoreActions<State, Actions, Props> = {
|
|
1742
|
+
- myAction: ({s, args: [v]}) => ({ ...s, myProp: v })
|
|
1743
|
+
-}
|
|
1744
|
+
-
|
|
1745
|
+
- const options: StoreOptions<State, Action, Props> = {
|
|
1746
|
+
- onMount: () => {},
|
|
1747
|
+
- onPropsChange: {
|
|
1748
|
+
- getDeps: () => [],
|
|
1749
|
+
- effects: ({s, p}) => ({ ...s, myProp: p.prop }),
|
|
1750
|
+
- }
|
|
1751
|
+
- };
|
|
1752
|
+
-
|
|
1753
|
+
- function MyComponent(p:Props) {
|
|
1754
|
+
- const {state, actions} = useLocalStore(getInitialState, actions, props, options, [logEffects()]);
|
|
1755
|
+
- return <div>{myProp}</div>;
|
|
1756
|
+
- }
|
|
1757
|
+
|
|
1758
|
+
+ const config:StoreConfig<State, Actions, Props> = {
|
|
1759
|
+
+ getInitialState: () => ({ myProp: '' }),
|
|
1760
|
+
+ actions: {
|
|
1761
|
+
+ myAction: ({args: [v]}) => ({ myProp: v})
|
|
1762
|
+
+ // or
|
|
1763
|
+
+ myAction: setArgIn('myProp'),
|
|
1764
|
+
+ },
|
|
1765
|
+
+ onMount: () => {},
|
|
1766
|
+
+ onPropsChange: {
|
|
1767
|
+
+ getDeps: () => [],
|
|
1768
|
+
+ effects: ({ p }) => ({ myProp: p.prop })
|
|
1769
|
+
+ },
|
|
1770
|
+
+ middlewares: [logEffects()],
|
|
1771
|
+
+ }
|
|
1772
|
+
+
|
|
1773
|
+
+ function MyComponent(p:Props) {
|
|
1774
|
+
+ const {state, actions} = useLocalStore(config);
|
|
1775
|
+
+ return <div>{myProp}</div>;
|
|
1776
|
+
+ }
|
|
1777
|
+
```
|
|
1778
|
+
|
|
1779
|
+
## V5 to V6
|
|
1780
|
+
|
|
1781
|
+
### Step 1: v5 compatibility layer
|
|
1583
1782
|
|
|
1584
1783
|
To use the new engine while still using v5 API, juste import the v5 compatibility layer.
|
|
1585
1784
|
You can new store in new code.
|
|
@@ -1600,26 +1799,26 @@ setGlobalConfig({
|
|
|
1600
1799
|
});
|
|
1601
1800
|
```
|
|
1602
1801
|
|
|
1603
|
-
|
|
1802
|
+
### Step 2: Migrate code but keep v5 tests
|
|
1604
1803
|
|
|
1605
|
-
|
|
1804
|
+
#### Code
|
|
1606
1805
|
|
|
1607
|
-
|
|
1806
|
+
##### Types
|
|
1608
1807
|
|
|
1609
1808
|
| Before | After |
|
|
1610
1809
|
| --------------------- | ------------ |
|
|
1611
1810
|
| StateDecoratorActions | StoreActions |
|
|
1612
1811
|
| StateDecoratorOptions | StoreOptions |
|
|
1613
1812
|
|
|
1614
|
-
|
|
1813
|
+
##### Optimistic actions
|
|
1615
1814
|
|
|
1616
1815
|
If you are using optimistic effects, make sure to set the **optimisticActions** middleware to your stores, see [Optimistic actions](#optimistic-actions).
|
|
1617
1816
|
|
|
1618
|
-
|
|
1817
|
+
#### Hooks
|
|
1619
1818
|
|
|
1620
1819
|
Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
1621
1820
|
|
|
1622
|
-
|
|
1821
|
+
#### Actions
|
|
1623
1822
|
|
|
1624
1823
|
- rename action properties
|
|
1625
1824
|
|
|
@@ -1660,9 +1859,9 @@ Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
|
1660
1859
|
};
|
|
1661
1860
|
```
|
|
1662
1861
|
|
|
1663
|
-
|
|
1862
|
+
#### Options
|
|
1664
1863
|
|
|
1665
|
-
|
|
1864
|
+
##### onMount
|
|
1666
1865
|
|
|
1667
1866
|
```diff
|
|
1668
1867
|
- const options: StateDecoratorOptions<S, A, P> = {
|
|
@@ -1674,7 +1873,7 @@ Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
|
1674
1873
|
+} ;
|
|
1675
1874
|
```
|
|
1676
1875
|
|
|
1677
|
-
|
|
1876
|
+
##### logEnabled
|
|
1678
1877
|
|
|
1679
1878
|
```diff
|
|
1680
1879
|
- function Container(props: Props) {
|
|
@@ -1690,7 +1889,7 @@ Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
|
1690
1889
|
+ }
|
|
1691
1890
|
```
|
|
1692
1891
|
|
|
1693
|
-
|
|
1892
|
+
##### Props changes
|
|
1694
1893
|
|
|
1695
1894
|
```diff
|
|
1696
1895
|
- const options: StateDecoratorOptions<S, A, P> = {
|
|
@@ -1712,7 +1911,7 @@ Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
|
1712
1911
|
+} ;
|
|
1713
1912
|
```
|
|
1714
1913
|
|
|
1715
|
-
|
|
1914
|
+
#### Reuse v5 tests with v6 actions
|
|
1716
1915
|
|
|
1717
1916
|
```diff
|
|
1718
1917
|
- import { testSyncAction, testAsyncAction } from 'state-decorator';
|
|
@@ -1720,7 +1919,7 @@ Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
|
|
|
1720
1919
|
+ import { testV6SyncAction, testV6AsyncAction } from 'state-decorator/v5_test';
|
|
1721
1920
|
```
|
|
1722
1921
|
|
|
1723
|
-
|
|
1922
|
+
### Step 3 (optional): Migrate tests
|
|
1724
1923
|
|
|
1725
1924
|
🎊 Congratulations! 🎊
|
|
1726
1925
|
|
|
@@ -1768,34 +1967,27 @@ You can add this snippet to quickly create a stateful functional component using
|
|
|
1768
1967
|
"import React from 'react';",
|
|
1769
1968
|
"import { useStore, StoreActions, LoadingProps } from 'state-decorator';",
|
|
1770
1969
|
"",
|
|
1771
|
-
"export type
|
|
1772
|
-
"",
|
|
1773
|
-
"export type $1State = {};",
|
|
1970
|
+
"export type Props = {};",
|
|
1774
1971
|
"",
|
|
1775
|
-
"export type
|
|
1972
|
+
"export type State = {};",
|
|
1776
1973
|
"",
|
|
1777
|
-
"type
|
|
1778
|
-
"type State = $1State;",
|
|
1779
|
-
"type Actions = $1Actions;",
|
|
1974
|
+
"export type Actions = {};",
|
|
1780
1975
|
"",
|
|
1781
1976
|
"type ViewProps = Props & Actions & State & Pick<LoadingProps<Actions>, 'loadingMap'>;",
|
|
1782
1977
|
"",
|
|
1783
|
-
"export
|
|
1784
|
-
"
|
|
1978
|
+
"export const storeConfig: StoreConfig<State, Actions, Props> = {",
|
|
1979
|
+
" getInitialState: (p:$1Props) => ({",
|
|
1980
|
+
" }),",
|
|
1981
|
+
" actions: {",
|
|
1982
|
+
" }",
|
|
1785
1983
|
"};",
|
|
1786
1984
|
"",
|
|
1787
1985
|
"export function $1View(p: ViewProps) {",
|
|
1788
1986
|
" return (<div>[$1View]</div>);",
|
|
1789
1987
|
"}",
|
|
1790
1988
|
"",
|
|
1791
|
-
"export const actions$1: StoreActions<State, Actions, Props> = {};",
|
|
1792
|
-
"",
|
|
1793
|
-
"export function onMount(actions:$1Actions) {}",
|
|
1794
|
-
"",
|
|
1795
|
-
"const store = createStore(getInitialState, actions$1, { onMount });",
|
|
1796
|
-
"",
|
|
1797
1989
|
"export default React.memo(function $1(p: $1Props) {",
|
|
1798
|
-
" const { state:s, actions:a, loadingMap } =
|
|
1990
|
+
" const { state:s, actions:a, loadingMap } = useLocalStore(config, p);",
|
|
1799
1991
|
" return <$1View {...p} {...s} {...a} loadingMap={loadingMap} />;",
|
|
1800
1992
|
"});"
|
|
1801
1993
|
],
|