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 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 (uses pure functions, utility functions provided)
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
- - Enforce separation of container components and presentation components.
19
- - Strongly typed
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 React from 'react';
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
- export const getInitialState = (): State => ({
79
- selectedUserId: null,
80
- user: null,
81
- });
82
-
83
- export const userAppActions: StoreActions<State, Actions, Props> = {
84
- // synchronous action (simple form)
85
- selectUser: ({ state, args: [id] }) => ({ ...state, selectedUserId: id }),
86
-
87
- // a synchronous action with side effects
88
- selectAndLoadUser: {
89
- // same as simple form, using aliases (use short context alias 's' instead of 'state')
90
- // effects contain only immutable state changes
91
- effects: ({ s, args: [id] }) => ({ ...s, selectedUserId: id }),
92
- // side effects (ie. other effects than state changes), using "actions" alias
93
- sideEffects: ({ a }) => {
94
- 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
+ },
95
104
  },
96
- },
97
105
 
98
- // asynchronous action
99
- loadUser: {
100
- // promise provider, can use action context (ctx) to get current state, args, props etc.
101
- getPromise: (ctx) => Promise.resolve({ firstName: 'John', lastName: 'Doe' }),
102
- // state changes
103
- effects: ({ s, res }) => ({ ...s, user: res }),
104
- // state changes if error
105
- errorEffects: ({ s, error }) => ({ ...s, user: null }),
106
- // side effects here
107
- sideEffects: ({ res, actions }) => {},
108
- // side effects if error
109
- errorSideEffects: ({ err, actions }) => {},
110
- // 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
+ },
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(getInitialState, userAppActions, props);
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 | Purpose | Component refreshed on store change | Store is destroyed on unmount |
143
- | ------------- | ------------------------------------------------------ | ----------------------------------- | ----------------------------- |
144
- | useLocalStore | Create a store and binds it to the react component | Y | Y |
145
- | useStore | Binds an existing store to a react component (sharing) | Y | Y |
146
- | useBindStore | Bind an existing store to a react component (sharing) | N | Y |
147
- | useStoreSlice | Binds a store slice to a react component (sharing) | If slice has changed only | N |
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
- export const getInitialState = (p: Props): State => ({
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(getInitialState, actionsImpl);
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 changes
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 actions: StoreActions<State, Actions> = {
201
- setText: ({ s, args: [newText] }) => ({ ...s, text: newText }),
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 actions: StoreActions<State, Actions> = {
225
- setText: {
226
- effects: ({ s, args: [newText] }) => ({ ...s, text: newText }),
227
- // use either debounceTimeout or debounceSideEffectsTimeout
228
- // debounceTimeout: 5000,
229
- debounceSideEffectsTimeout: 5000,
230
- sideEffects: ({ a }) => {
231
- 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
+ },
232
248
  },
233
249
  },
234
250
  };
@@ -270,14 +286,16 @@ action: {
270
286
  Example:
271
287
 
272
288
  ```typescript
273
- const actions: StoreActions<State, Actions> = {
274
- loadList: {
275
- getPromise: () => fetch('https://myapi/myservices').then((res) => (res.ok ? res.json() : Promise.reject())),
276
- effects: ({ s, res }) => ({ ...s, list: res }),
277
- sideEffects: ({ a }) => {
278
- 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',
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 actions: StoreActions<State, Actions> = {
379
- loadList: {
380
- getPromise: () => new Promise((_, reject) => setTimeout(reject, 500, new Error('Too bad'))),
381
- getSuccessMessage: () => 'success!',
382
- 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
+ },
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: ({ state, error }) => ({ ...state, error: error.message, list: [] }),
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 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);
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 actions: StoreActions<State, Actions> = {
521
- deleteItem: {
522
- getPromise: ([id]) => new Promise((resolve) => setTimeout(resolve, 500)),
523
- optimisticEffects: (state, args) => ({ ...state, list: list.filter((item) => item.id === args[0]) }),
524
- errorEffects: (state, error: Error) => ({ ...state, error: 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
+ },
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
- export const actionsAbort: StoreActions<State, Actions, Props> = {
549
- onAction: {
550
- abortable: true,
551
- preEffects: () => ({ isError: false, isSuccess: false, isAborted: false }),
552
- getPromise: ({s, args: [willCrash] abortSignal}) =>
553
- new Promise((resolve, reject) => {
554
- const timeout = window.setTimeout(willCrash ? reject : resolve, 2500, willCrash ? new Error('boom') : 'result');
555
- abortSignal.addEventListener('abort', () => {
556
- window.clearTimeout(timeout);
557
- reject(new DOMException('Aborted', 'AbortError'));
558
- });
559
- }),
560
- errorEffects: ({s, err}) => (err.name === 'AbortError' ? { ...s, isAborted: true } : { ...s, isError: true }),
561
- effects: ({s}) => ({ ...s, isSuccess: true }),
562
- errorSideEffects: ({s, err}) => {
563
- if (e.name === 'AbortError') {
564
- console.log('AbortError side effect');
565
- } else {
566
- console.log('Other error side effect');
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
  [![Edit Abort](https://codesandbox.io/static/img/play-codesandbox.svg)](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 { createStore } from 'state-decorator';
652
+ import { StoreConfig } from 'state-decorator';
583
653
 
584
- const store = createStore(getInitialState, actionsImpl, {
585
- onMount: ({ a }) => {
586
- a.loadList;
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 { createStore } from 'state-decorator';
686
+ import { StoreConfig } from 'state-decorator';
599
687
 
600
- const store = createStore(getInitialState, actionsImpl, {
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(getInitialState, userAppActions);
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
  [![Edit Slice](https://codesandbox.io/static/img/play-codesandbox.svg)](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 store = createStore(getInitialState, actionsImpl, {
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: ({ state, props, indices, isInit }) => ({ ...state, selectedId: props.id }),
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 { createStore } from 'state-decorator';
776
+ import { StoreConfig } from 'state-decorator';
713
777
 
714
- const store = createStore(getInitialState, actionsImpl, {
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: ({ state, props, indices }) => ({ ...state, otherProp: props.otherProp }),
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
- const store = createStore(getInitialState, actionsImpl, {
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: ({ s, p }) => ({ ...s, userId: p.userId }),
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
- const store = createStore(getInitialState, actionsImpl, {
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: ({ s, p, isInit }) => (isInit ? null : { ...s, userId: p.userId }),
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
+ [![Edit Slice](https://codesandbox.io/static/img/play-codesandbox.svg)](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
- derivedState: {
812
- derivedProp: {
813
- // get the list of dependencies to be checked to trigger the computation of the derived state
814
- // if state.value AND/OR props.prop1 changes, derivedProp is recomputed
815
- getDeps: ({ state, props }) => [state.value, props.propIn],
816
- // compute derived state from state & props (use short aliases)
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, actionsImpl, props, { name: 'My Store' }, [logEffects()]);
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, actionsImpl, props, options, [logMiddleware]);
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, actionsImpl, props, { name: 'My Store' }, [
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, actionsImpl, props, { name: 'My Store' }, [devtools()]);
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
- ## Step 1: v5 compatibility layer
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
- ## Step 2: Migrate code but keep v5 tests
1801
+ ### Step 2: Migrate code but keep v5 tests
1444
1802
 
1445
- ### Code
1803
+ #### Code
1446
1804
 
1447
- #### Types
1805
+ ##### Types
1448
1806
 
1449
1807
  | Before | After |
1450
1808
  | --------------------- | ------------ |
1451
1809
  | StateDecoratorActions | StoreActions |
1452
1810
  | StateDecoratorOptions | StoreOptions |
1453
1811
 
1454
- #### Optimistic actions
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
- ### Hooks
1816
+ #### Hooks
1459
1817
 
1460
1818
  Choose one of _useLocalStore_, _useStore_, _useBindStore_ or _useStoreSlice_.
1461
1819
 
1462
- ### Actions
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
- ### Options
1861
+ #### Options
1504
1862
 
1505
- #### onMount
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
- #### logEnabled
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
- #### Props changes
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
- ### Reuse v5 tests with v6 actions
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
- ## Step 3 (optional): Migrate tests
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 $1Props = {};",
1969
+ "export type Props = {};",
1612
1970
  "",
1613
- "export type $1State = {};",
1971
+ "export type State = {};",
1614
1972
  "",
1615
- "export type $1Actions = {};",
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 function getInitialState(p:$1Props): State {",
1624
- "return {};",
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 } = useStore(store, p);",
1989
+ " const { state:s, actions:a, loadingMap } = useLocalStore(config, p);",
1639
1990
  " return <$1View {...p} {...s} {...a} loadingMap={loadingMap} />;",
1640
1991
  "});"
1641
1992
  ],