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.
Files changed (65) hide show
  1. package/README.md +384 -192
  2. package/es/helpers.d.ts +66 -0
  3. package/es/helpers.js +163 -0
  4. package/{dist/v5_test.js.map → es/helpers.js.map} +1 -1
  5. package/es/impl.d.ts +19 -5
  6. package/es/impl.js +963 -1
  7. package/es/impl.js.map +1 -1
  8. package/es/index.d.ts +34 -36
  9. package/es/index.js +1013 -1
  10. package/es/index.js.map +1 -1
  11. package/es/middlewares.js +519 -1
  12. package/es/middlewares.js.map +1 -1
  13. package/es/test.d.ts +6 -4
  14. package/es/test.js +349 -1
  15. package/es/test.js.map +1 -1
  16. package/es/types.d.ts +32 -3
  17. package/es/v5.js +301 -1
  18. package/es/v5.js.map +1 -1
  19. package/es/v5_test.js +392 -1
  20. package/es/v5_test.js.map +1 -1
  21. package/es/v6.d.ts +18 -0
  22. package/es/v6.js +98 -0
  23. package/es/v6.js.map +1 -0
  24. package/helpers.d.ts +66 -0
  25. package/helpers.js +175 -0
  26. package/{dist/test.js.map → helpers.js.map} +1 -1
  27. package/impl.d.ts +19 -5
  28. package/impl.js +996 -1
  29. package/impl.js.map +1 -1
  30. package/index.d.ts +34 -36
  31. package/index.js +1044 -1
  32. package/index.js.map +1 -1
  33. package/middlewares.js +526 -1
  34. package/middlewares.js.map +1 -1
  35. package/package.json +5 -4
  36. package/test.d.ts +6 -4
  37. package/test.js +358 -1
  38. package/test.js.map +1 -1
  39. package/types.d.ts +32 -3
  40. package/v5.js +322 -1
  41. package/v5.js.map +1 -1
  42. package/v5_test.js +416 -1
  43. package/v5_test.js.map +1 -1
  44. package/v6.d.ts +18 -0
  45. package/v6.js +123 -0
  46. package/v6.js.map +1 -0
  47. package/dist/impl.d.ts +0 -195
  48. package/dist/impl.js +0 -2
  49. package/dist/impl.js.map +0 -1
  50. package/dist/index.d.ts +0 -168
  51. package/dist/index.js +0 -2
  52. package/dist/index.js.map +0 -1
  53. package/dist/middlewares.d.ts +0 -26
  54. package/dist/middlewares.js +0 -2
  55. package/dist/middlewares.js.map +0 -1
  56. package/dist/shallow.d.ts +0 -15
  57. package/dist/test.d.ts +0 -134
  58. package/dist/test.js +0 -2
  59. package/dist/types.d.ts +0 -352
  60. package/dist/v5.d.ts +0 -58
  61. package/dist/v5.js +0 -2
  62. package/dist/v5.js.map +0 -1
  63. package/dist/v5_test.d.ts +0 -70
  64. package/dist/v5_test.js +0 -2
  65. 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 React from 'react';
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
- export const getInitialState = (): State => ({
86
- selectedUserId: null,
87
- user: null,
88
- });
89
-
90
- export const userAppActions: StoreActions<State, Actions, Props> = {
91
- // synchronous action (simple form)
92
- selectUser: ({ args: [id] }) => ({ selectedUserId: id }),
93
-
94
- // a synchronous action with side effects
95
- selectAndLoadUser: {
96
- // effects contain only immutable state changes
97
- effects: ({ args: [id] }) => ({ selectedUserId: id }),
98
- // side effects (ie. other effects than state changes), using "actions" alias
99
- sideEffects: ({ a }) => {
100
- a.loadUser();
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
- // asynchronous action
105
- loadUser: {
106
- // promise provider, can use action context (ctx) to get current state, args, props etc.
107
- getPromise: (ctx) => Promise.resolve({ firstName: 'John', lastName: 'Doe' }),
108
- // state changes
109
- effects: ({ res }) => ({ user: res }),
110
- // state changes if error
111
- errorEffects: ({ error }) => ({ user: null }),
112
- // side effects here
113
- sideEffects: ({ res, actions }) => {},
114
- // side effects if error
115
- errorSideEffects: ({ err, actions }) => {},
116
- // many more options available here, read the doc!
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(getInitialState, userAppActions, props);
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
- export const getInitialState = (p: Props): State => ({
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(getInitialState, actionsImpl);
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 actions: StoreActions<State, Actions> = {
206
- setText: ({ args: [newText] }) => ({ text: newText }),
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 actions: StoreActions<State, Actions> = {
230
- setText: {
231
- effects: ({ args: [newText] }) => ({ text: newText }),
232
- // use either debounceTimeout or debounceSideEffectsTimeout
233
- // debounceTimeout: 5000,
234
- debounceSideEffectsTimeout: 5000,
235
- sideEffects: ({ a }) => {
236
- a.saveDocument();
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 actions: StoreActions<State, Actions> = {
279
- loadList: {
280
- getPromise: () => fetch('https://myapi/myservices').then((res) => (res.ok ? res.json() : Promise.reject())),
281
- effects: ({ res }) => ({ list: res }),
282
- sideEffects: ({ a }) => {
283
- a.otherAction();
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 actions: StoreActions<State, Actions> = {
384
- loadList: {
385
- getPromise: () => new Promise((_, reject) => setTimeout(reject, 500, new Error('Too bad'))),
386
- getSuccessMessage: () => 'success!',
387
- getErrorMessage: ({ err }) => `Error: ${err.message}`,
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 store = createStore(getInitialState, actionsImpl, options, [optimisticActions()],
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 actions: StoreActions<State, Actions> = {
524
- deleteItem: {
525
- getPromise: ([id]) => new Promise((resolve) => setTimeout(resolve, 500)),
526
- optimisticEffects: ({ args: [id] }) => ({ list: list.filter((item) => item.id === id) }),
527
- errorEffects: ({err} => ({ err: error.message }),
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
- export const actionsAbort: StoreActions<State, Actions, Props> = {
552
- onAction: {
553
- abortable: true,
554
- preEffects: () => ({ isError: false, isSuccess: false, isAborted: false }),
555
- getPromise: ({s, args: [willCrash] abortSignal}) =>
556
- new Promise((resolve, reject) => {
557
- const timeout = window.setTimeout(willCrash ? reject : resolve, 2500, willCrash ? new Error('boom') : 'result');
558
- abortSignal.addEventListener('abort', () => {
559
- window.clearTimeout(timeout);
560
- reject(new DOMException('Aborted', 'AbortError'));
561
- });
562
- }),
563
- errorEffects: ({ err}) => (err.name === 'AbortError' ? { isAborted: true } : { isError: true }),
564
- effects: () => ({ isSuccess: true }),
565
- errorSideEffects: ({ err }) => {
566
- if (e.name === 'AbortError') {
567
- console.log('AbortError side effect');
568
- } else {
569
- console.log('Other error side effect');
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
  [![Edit Abort](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/abort-v6-uvnl7)
577
610
 
578
- # Derived state
611
+ ## Helper effect functions
579
612
 
580
- A derived state is a state that can be deduced from state and props.
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 { createStore } from 'state-decorator';
618
+ import { setArgIn } from 'state-decorator/helpers';
584
619
 
585
620
  type State = {
586
- value: number;
621
+ myProp: string;
587
622
  };
588
623
 
589
624
  type Actions = {
590
- add1: () => void;
591
- };
592
-
593
- type Props = {
594
- propIn: number;
625
+ setMyProp: (v: string) => void;
595
626
  };
596
627
 
597
- type DerivedState = {
598
- derivedProp: number;
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
- const store = createStore<State, Actions, Props, DerivedState>(
602
- () => ({ value: 0 }),
603
- {
604
- add1: ({ s }) => ({ ...s, value: s.value + 1 }),
605
- },
606
- {
607
- derivedState: {
608
- derivedProp: {
609
- // get the list of dependencies to be checked to trigger the computation of the derived state
610
- // if state.value AND/OR props.prop1 changes, derivedProp is recomputed
611
- getDeps: ({ state, props }) => [state.value, props.propIn],
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
- export function App(props: Props) {
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
- ## Recipes
649
+ When store is created (for example whe React component in mounted), the **onMount** option is called.
627
650
 
628
- - Derived state is the same as using _useMemo_ in sub component.
629
- - 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.
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
- # Call actions on mount
654
+ const config: StoreConfig<State, Actions, Props> = {
655
+ getInitialState,
656
+ actions,
657
+ onMount: ({ p }) => {
658
+ p.loadParentList();
659
+ },
660
+ };
661
+ ```
634
662
 
635
- When a hook that creates / binds a store is mounted, the **onMount** option is called.
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 { createStore } from 'state-decorator';
668
+ import { StoreConfig } from 'state-decorator';
639
669
 
640
- // global store
641
- const store = createStore(getInitialState, actionsImpl, {
642
- onMount: ({ a }) => {
643
- a.loadList;
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
- store.init(initialProps);
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 { createStore } from 'state-decorator';
705
+ import React from 'react';
706
+ import { useStore, useStoreSlice, StoreActions } from 'state-decorator';
652
707
 
653
- // local store
654
- function Container(p: ContainerProps) {
655
- const store = useLocalStore(getInitialState, actionsImpl, p, {
656
- onMount: ({ a }) => {
657
- a.loadList;
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
- If an initial action is launched each time a property changes, consider using the **onMount** flag of the **onPropsChange** entry.
740
+ [![Edit Slice](https://codesandbox.io/static/img/play-codesandbox.svg)](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 store = createStore(getInitialState, actionsImpl, {
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 { createStore } from 'state-decorator';
776
+ import { StoreConfig } from 'state-decorator';
698
777
 
699
- const store = createStore(getInitialState, actionsImpl, {
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
- const store = createStore(getInitialState, actionsImpl, {
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
- const store = createStore(getInitialState, actionsImpl, {
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(getInitialState, userAppActions, props);
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(getInitialState, actionsImpl, props);
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, actionsImpl, props, { name: 'My Store' }, [logEffects()]);
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, actionsImpl, props, options, [logMiddleware]);
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
- middlewares.ts:36 [StateDecorator sample] setProp1 effects {list: Array(10), prop1: "92", prop2: "", prop3: ""}
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, actionsImpl, props, { name: 'My Store' }, [
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, actionsImpl, props, { name: 'My Store' }, [devtools()]);
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(getInitialState, actions, props, options)
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
- ## Step 1: v5 compatibility layer
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
- ## Step 2: Migrate code but keep v5 tests
1802
+ ### Step 2: Migrate code but keep v5 tests
1604
1803
 
1605
- ### Code
1804
+ #### Code
1606
1805
 
1607
- #### Types
1806
+ ##### Types
1608
1807
 
1609
1808
  | Before | After |
1610
1809
  | --------------------- | ------------ |
1611
1810
  | StateDecoratorActions | StoreActions |
1612
1811
  | StateDecoratorOptions | StoreOptions |
1613
1812
 
1614
- #### Optimistic actions
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
- ### Hooks
1817
+ #### Hooks
1619
1818
 
1620
1819
  Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
1621
1820
 
1622
- ### Actions
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
- ### Options
1862
+ #### Options
1664
1863
 
1665
- #### onMount
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
- #### logEnabled
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
- #### Props changes
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
- ### Reuse v5 tests with v6 actions
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
- ## Step 3 (optional): Migrate tests
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 $1Props = {};",
1772
- "",
1773
- "export type $1State = {};",
1970
+ "export type Props = {};",
1774
1971
  "",
1775
- "export type $1Actions = {};",
1972
+ "export type State = {};",
1776
1973
  "",
1777
- "type Props = $1Props;",
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 function getInitialState(p:$1Props): State {",
1784
- "return {};",
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 } = useStore(store, p);",
1990
+ " const { state:s, actions:a, loadingMap } = useLocalStore(config, p);",
1799
1991
  " return <$1View {...p} {...s} {...a} loadingMap={loadingMap} />;",
1800
1992
  "});"
1801
1993
  ],