state-decorator 6.10.0 → 7.0.0-beta.0
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 +317 -140
- package/es/impl.js +1 -1
- package/es/index.d.ts +35 -37
- package/es/index.js +1 -1
- package/es/index.js.map +1 -1
- package/es/middlewares.js +1 -1
- package/es/types.d.ts +9 -9
- package/es/v5_test.js +1 -1
- package/impl.js +1 -1
- package/index.d.ts +35 -37
- package/index.js +1 -1
- package/middlewares.js +1 -1
- package/package.json +1 -1
- package/types.d.ts +9 -9
- package/v5_test.js +1 -1
package/README.md
CHANGED
|
@@ -11,12 +11,19 @@ The StateDecorator is a set of Reacts hook that manages a complex component stat
|
|
|
11
11
|
- Deterministic state changes (clear separation of effects and side effects)
|
|
12
12
|
- Ease asynchronous actions state changes (loading / error states, success & error handlers, parallel actions management, optimistic updates...)
|
|
13
13
|
- Easily and efficiently share slices of state.
|
|
14
|
-
- Easily testable (
|
|
14
|
+
- Easily testable (test framework provided)
|
|
15
15
|
- Easily update state from or react to props changes
|
|
16
|
-
- Ease debugging (trace state changes)
|
|
16
|
+
- Ease debugging (trace state changes, dev tools)
|
|
17
17
|
- Improve code conciseness (no boiler plate code)
|
|
18
|
-
-
|
|
19
|
-
|
|
18
|
+
- Strongly typed (Typescript)
|
|
19
|
+
|
|
20
|
+
# V7: Partial state and new hooks
|
|
21
|
+
|
|
22
|
+
- All effects can now return a partial state that will be merged with current state.
|
|
23
|
+
- More documentation on global and local store use cases.
|
|
24
|
+
- **useLocalStore** and **useStore** now have a parameter to specify if the React component must re-rerender after a state change.
|
|
25
|
+
- New **useStoreContextSlice** hook was added to get a slice of a store in a context.
|
|
26
|
+
- Removed useBindStore hook.
|
|
20
27
|
|
|
21
28
|
# 🏎️ V6: The Store
|
|
22
29
|
|
|
@@ -82,13 +89,12 @@ export const getInitialState = (): State => ({
|
|
|
82
89
|
|
|
83
90
|
export const userAppActions: StoreActions<State, Actions, Props> = {
|
|
84
91
|
// synchronous action (simple form)
|
|
85
|
-
selectUser: ({
|
|
92
|
+
selectUser: ({ args: [id] }) => ({ selectedUserId: id }),
|
|
86
93
|
|
|
87
94
|
// a synchronous action with side effects
|
|
88
95
|
selectAndLoadUser: {
|
|
89
|
-
// same as simple form, using aliases (use short context alias 's' instead of 'state')
|
|
90
96
|
// effects contain only immutable state changes
|
|
91
|
-
effects: ({
|
|
97
|
+
effects: ({ args: [id] }) => ({ selectedUserId: id }),
|
|
92
98
|
// side effects (ie. other effects than state changes), using "actions" alias
|
|
93
99
|
sideEffects: ({ a }) => {
|
|
94
100
|
a.loadUser();
|
|
@@ -100,9 +106,9 @@ export const userAppActions: StoreActions<State, Actions, Props> = {
|
|
|
100
106
|
// promise provider, can use action context (ctx) to get current state, args, props etc.
|
|
101
107
|
getPromise: (ctx) => Promise.resolve({ firstName: 'John', lastName: 'Doe' }),
|
|
102
108
|
// state changes
|
|
103
|
-
effects: ({
|
|
109
|
+
effects: ({ res }) => ({ user: res }),
|
|
104
110
|
// state changes if error
|
|
105
|
-
errorEffects: ({
|
|
111
|
+
errorEffects: ({ error }) => ({ user: null }),
|
|
106
112
|
// side effects here
|
|
107
113
|
sideEffects: ({ res, actions }) => {},
|
|
108
114
|
// side effects if error
|
|
@@ -139,12 +145,12 @@ export function App(props: Props) => {
|
|
|
139
145
|
|
|
140
146
|
# React hooks
|
|
141
147
|
|
|
142
|
-
| Hook
|
|
143
|
-
|
|
|
144
|
-
| useLocalStore
|
|
145
|
-
| useStore
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
+
| Hook | Purpose | Returns | Is component refreshed on store change? | Is store destroyed on unmount? |
|
|
149
|
+
| -------------------- | --------------------------------------------------- | ------- | --------------------------------------- | ------------------------------ |
|
|
150
|
+
| useLocalStore | Create a store and binds it to the react component. | Store | if **refreshOnUpdate** is set | Y |
|
|
151
|
+
| useStore | Binds a store instance to the react component. | Store | if **refreshOnUpdate** is set | N |
|
|
152
|
+
| useStoreSlice | Get a slice of a store. | Slice | If slice has changed only | N |
|
|
153
|
+
| useStoreContextSlice | Get a slice of a store stored in a context | Slice | If slice has changed only | N |
|
|
148
154
|
|
|
149
155
|
# Initial state
|
|
150
156
|
|
|
@@ -190,14 +196,14 @@ The StateDecorator is taking a list of actions and decorate them to inject state
|
|
|
190
196
|
## Short form
|
|
191
197
|
|
|
192
198
|
```typescript
|
|
193
|
-
action: (ctx) => ctx.state, // state
|
|
199
|
+
action: (ctx) => ctx.state, // changes to be merged to current state
|
|
194
200
|
```
|
|
195
201
|
|
|
196
202
|
Example:
|
|
197
203
|
|
|
198
204
|
```typescript
|
|
199
205
|
const actions: StoreActions<State, Actions> = {
|
|
200
|
-
setText: ({
|
|
206
|
+
setText: ({ args: [newText] }) => ({ text: newText }),
|
|
201
207
|
};
|
|
202
208
|
```
|
|
203
209
|
|
|
@@ -222,7 +228,7 @@ Example:
|
|
|
222
228
|
```typescript
|
|
223
229
|
const actions: StoreActions<State, Actions> = {
|
|
224
230
|
setText: {
|
|
225
|
-
effects: ({
|
|
231
|
+
effects: ({ args: [newText] }) => ({ text: newText }),
|
|
226
232
|
// use either debounceTimeout or debounceSideEffectsTimeout
|
|
227
233
|
// debounceTimeout: 5000,
|
|
228
234
|
debounceSideEffectsTimeout: 5000,
|
|
@@ -272,7 +278,7 @@ Example:
|
|
|
272
278
|
const actions: StoreActions<State, Actions> = {
|
|
273
279
|
loadList: {
|
|
274
280
|
getPromise: () => fetch('https://myapi/myservices').then((res) => (res.ok ? res.json() : Promise.reject())),
|
|
275
|
-
effects: ({
|
|
281
|
+
effects: ({ res }) => ({ list: res }),
|
|
276
282
|
sideEffects: ({ a }) => {
|
|
277
283
|
a.otherAction();
|
|
278
284
|
},
|
|
@@ -395,7 +401,7 @@ If some actions need to be triggered, set **errorSideEffects**.
|
|
|
395
401
|
const actions: StoreActions<State, Actions> = {
|
|
396
402
|
loadList: {
|
|
397
403
|
getPromise: () => new Promise((_, reject) => setTimeout(reject, 500, new Error('Too bad'))),
|
|
398
|
-
errorEffects: ({
|
|
404
|
+
errorEffects: ({ error }) => ({ error: error.message, list: [] }),
|
|
399
405
|
getErrorMessage: ({ err }) => err.message,
|
|
400
406
|
errorSideEffects: ({ error }) => {
|
|
401
407
|
Logger.log(error);
|
|
@@ -517,8 +523,8 @@ Example:
|
|
|
517
523
|
const actions: StoreActions<State, Actions> = {
|
|
518
524
|
deleteItem: {
|
|
519
525
|
getPromise: ([id]) => new Promise((resolve) => setTimeout(resolve, 500)),
|
|
520
|
-
optimisticEffects: (
|
|
521
|
-
errorEffects: (
|
|
526
|
+
optimisticEffects: ({ args: [id] }) => ({ list: list.filter((item) => item.id === id) }),
|
|
527
|
+
errorEffects: ({err} => ({ err: error.message }),
|
|
522
528
|
},
|
|
523
529
|
};
|
|
524
530
|
```
|
|
@@ -554,9 +560,9 @@ export const actionsAbort: StoreActions<State, Actions, Props> = {
|
|
|
554
560
|
reject(new DOMException('Aborted', 'AbortError'));
|
|
555
561
|
});
|
|
556
562
|
}),
|
|
557
|
-
errorEffects: ({
|
|
558
|
-
effects: (
|
|
559
|
-
errorSideEffects: ({
|
|
563
|
+
errorEffects: ({ err}) => (err.name === 'AbortError' ? { isAborted: true } : { isError: true }),
|
|
564
|
+
effects: () => ({ isSuccess: true }),
|
|
565
|
+
errorSideEffects: ({ err }) => {
|
|
560
566
|
if (e.name === 'AbortError') {
|
|
561
567
|
console.log('AbortError side effect');
|
|
562
568
|
} else {
|
|
@@ -569,96 +575,92 @@ export const actionsAbort: StoreActions<State, Actions, Props> = {
|
|
|
569
575
|
|
|
570
576
|
[](https://codesandbox.io/s/abort-v6-uvnl7)
|
|
571
577
|
|
|
572
|
-
|
|
578
|
+
# Derived state
|
|
573
579
|
|
|
574
|
-
|
|
580
|
+
A derived state is a state that can be deduced from state and props.
|
|
575
581
|
|
|
576
582
|
```typescript
|
|
577
583
|
import { createStore } from 'state-decorator';
|
|
578
584
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
},
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
// onMount is called when this component is mounted
|
|
586
|
-
function Container(p: ContainerProps) {
|
|
587
|
-
useBindStore(store, p);
|
|
588
|
-
}
|
|
589
|
-
```
|
|
585
|
+
type State = {
|
|
586
|
+
value: number;
|
|
587
|
+
};
|
|
590
588
|
|
|
591
|
-
|
|
589
|
+
type Actions = {
|
|
590
|
+
add1: () => void;
|
|
591
|
+
};
|
|
592
592
|
|
|
593
|
-
|
|
593
|
+
type Props = {
|
|
594
|
+
propIn: number;
|
|
595
|
+
};
|
|
594
596
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
597
|
+
type DerivedState = {
|
|
598
|
+
derivedProp: number;
|
|
599
|
+
};
|
|
598
600
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
+
);
|
|
602
618
|
|
|
603
|
-
|
|
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
|
+
```
|
|
604
625
|
|
|
605
|
-
|
|
626
|
+
## Recipes
|
|
606
627
|
|
|
607
|
-
|
|
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
|
|
608
632
|
|
|
609
|
-
|
|
633
|
+
# Call actions on mount
|
|
610
634
|
|
|
611
|
-
|
|
612
|
-
const { state, actions } = useStore(store, props);
|
|
635
|
+
When a hook that creates / binds a store is mounted, the **onMount** option is called.
|
|
613
636
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
// if you are not interested in getting the state here
|
|
637
|
+
```typescript
|
|
638
|
+
import { createStore } from 'state-decorator';
|
|
617
639
|
|
|
618
|
-
|
|
619
|
-
|
|
640
|
+
// global store
|
|
641
|
+
const store = createStore(getInitialState, actionsImpl, {
|
|
642
|
+
onMount: ({ a }) => {
|
|
643
|
+
a.loadList;
|
|
644
|
+
},
|
|
645
|
+
});
|
|
620
646
|
|
|
621
|
-
|
|
622
|
-
|
|
647
|
+
store.init(initialProps);
|
|
648
|
+
```
|
|
623
649
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return <div>{s.text}</div>;
|
|
627
|
-
}
|
|
650
|
+
```typescript
|
|
651
|
+
import { createStore } from 'state-decorator';
|
|
628
652
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
653
|
+
// local store
|
|
654
|
+
function Container(p: ContainerProps) {
|
|
655
|
+
const store = useLocalStore(getInitialState, actionsImpl, p, {
|
|
656
|
+
onMount: ({ a }) => {
|
|
657
|
+
a.loadList;
|
|
658
|
+
},
|
|
659
|
+
});
|
|
632
660
|
}
|
|
633
661
|
```
|
|
634
662
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
# Global configuration
|
|
638
|
-
|
|
639
|
-
Overrides configuration to set properties that will be used by all stores.
|
|
640
|
-
|
|
641
|
-
```typescript
|
|
642
|
-
// all parameters are optional
|
|
643
|
-
setGlobalConfig({
|
|
644
|
-
// Used to clone state and props when managing optimistic conflicting actions.
|
|
645
|
-
clone: defaultCloneFunc,
|
|
646
|
-
// Compare to objets and returns if they are equal (slices).
|
|
647
|
-
comparator: shallow,
|
|
648
|
-
// Callback function to handle asychronous actions rejected promise (error reporting).
|
|
649
|
-
asyncErrorHandler: () => {},
|
|
650
|
-
// Tests if the error will trigger a retry of the action or will fail directly (retry promises)
|
|
651
|
-
retryOnErrorFunction: (error: Error) => error instanceof TypeError,
|
|
652
|
-
// Notification function on successful asynchronous action (if success message is set on action)
|
|
653
|
-
notifySuccess: undefined,
|
|
654
|
-
// Notification function on failed asynchronous action (if error message is set on action)
|
|
655
|
-
notifyError: undefined,
|
|
656
|
-
// Notification function injected in side effects action context (success and error) to notify warning
|
|
657
|
-
notifyWarning: undefined,
|
|
658
|
-
// 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_.
|
|
659
|
-
getErrorMessage: undefined,
|
|
660
|
-
});
|
|
661
|
-
```
|
|
663
|
+
If an initial action is launched each time a property changes, consider using the **onMount** flag of the **onPropsChange** entry.
|
|
662
664
|
|
|
663
665
|
# Update store when props change
|
|
664
666
|
|
|
@@ -672,7 +674,7 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
672
674
|
// list of depedencies that should trigger effects & side effects if changed
|
|
673
675
|
getDeps: (p) => [p.id],
|
|
674
676
|
// state changes, indices contains the indices of dependencies that have changed
|
|
675
|
-
effects: ({
|
|
677
|
+
effects: ({ props, indices, isInit }) => ({ selectedId: props.id }),
|
|
676
678
|
// other changes (use short aliases)
|
|
677
679
|
sideEffects: ({ p, a, indices, isInit }) => {
|
|
678
680
|
a.loadItem(p.id);
|
|
@@ -708,7 +710,7 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
708
710
|
},
|
|
709
711
|
{
|
|
710
712
|
getDeps: (p) => [p.otherProp],
|
|
711
|
-
effects: ({
|
|
713
|
+
effects: ({ props, indices }) => ({ otherProp: props.otherProp }),
|
|
712
714
|
sideEffects: ({ p, a }) => {
|
|
713
715
|
a.otherSideEffect(p.otherProp);
|
|
714
716
|
},
|
|
@@ -731,7 +733,7 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
731
733
|
onPropsChange: [
|
|
732
734
|
{
|
|
733
735
|
getDeps: (p) => [p.userId],
|
|
734
|
-
effects: ({
|
|
736
|
+
effects: ({ p }) => ({ userId: p.userId }),
|
|
735
737
|
sideEffects: ({ p, a }) => {
|
|
736
738
|
a.loadUser(p.userId);
|
|
737
739
|
},
|
|
@@ -745,7 +747,7 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
745
747
|
onPropsChange: [
|
|
746
748
|
{
|
|
747
749
|
getDeps: (p) => [p.userId],
|
|
748
|
-
effects: ({
|
|
750
|
+
effects: ({ p, isInit }) => (isInit ? null : { userId: p.userId }),
|
|
749
751
|
sideEffects: ({ p, a }) => {
|
|
750
752
|
a.loadUser(p.userId);
|
|
751
753
|
},
|
|
@@ -762,60 +764,235 @@ const store = createStore(getInitialState, actionsImpl, {
|
|
|
762
764
|
- Use several prop change configurations to separate dependencies between props and have simple definition.
|
|
763
765
|
- Use onMount flag to have a more systematic and simpler code.
|
|
764
766
|
|
|
765
|
-
#
|
|
767
|
+
# Global / local Stores and State sharing
|
|
766
768
|
|
|
767
|
-
|
|
769
|
+
There are two ways to share state and actions:
|
|
770
|
+
|
|
771
|
+
- Using regular props.
|
|
772
|
+
- Using state slices.
|
|
773
|
+
|
|
774
|
+
There are two types of stores:
|
|
775
|
+
|
|
776
|
+
- Global store:
|
|
777
|
+
|
|
778
|
+
- store is created as an instance (**createStore**) and it is exported from the JS module.
|
|
779
|
+
- it can be imported in any other JS module.
|
|
780
|
+
- store lifespan is equal to the application one.
|
|
781
|
+
|
|
782
|
+
- Local store:
|
|
783
|
+
- store is created by a hook (**useLocalStore**) and is bound to a React component (each instance of this component owns its own store).
|
|
784
|
+
- action and props can be shared using props or sharing the store itself (see example below)
|
|
785
|
+
- store is destroyed when its owner React component is unmounted.
|
|
786
|
+
|
|
787
|
+
## Using props
|
|
788
|
+
|
|
789
|
+
- Bind a React component to make store state / actions available on React component.
|
|
790
|
+
- Any change in the store (state, loading actions, ...) will trigger a refresh of the bound React component.
|
|
791
|
+
- Pass state / actions using regular React props.
|
|
792
|
+
- Use React.memo to prevent unecessary React re-renders if needed.
|
|
793
|
+
|
|
794
|
+
### Global store
|
|
768
795
|
|
|
769
796
|
```typescript
|
|
770
|
-
|
|
797
|
+
// Declare typings & actions as above
|
|
771
798
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
};
|
|
799
|
+
// Create a store in a dedicated file, named GlobalStore.ts for example
|
|
800
|
+
export const store = createStore(getInitialState, userAppActions);
|
|
775
801
|
|
|
776
|
-
|
|
777
|
-
add1: () => void;
|
|
778
|
-
};
|
|
802
|
+
//----------------------
|
|
779
803
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
};
|
|
804
|
+
// In another file
|
|
805
|
+
import React, { memo } from 'react';
|
|
806
|
+
import { useStore } from 'state-decorator';
|
|
807
|
+
import store from './GlobalStore';
|
|
783
808
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
809
|
+
// Bind to react component
|
|
810
|
+
// Each time the store state changes, a re-render is done.
|
|
811
|
+
export function Container(props: Props) {
|
|
812
|
+
const { state, actions } = useStore(store, props);
|
|
813
|
+
return (
|
|
814
|
+
<div>
|
|
815
|
+
<Title text={state.title} />
|
|
816
|
+
<Subtitle text={state.subtitle} />
|
|
817
|
+
</div>
|
|
818
|
+
);
|
|
819
|
+
}
|
|
787
820
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
{
|
|
794
|
-
derivedState: {
|
|
795
|
-
derivedProp: {
|
|
796
|
-
// get the list of dependencies to be checked to trigger the computation of the derived state
|
|
797
|
-
// if state.value AND/OR props.prop1 changes, derivedProp is recomputed
|
|
798
|
-
getDeps: ({ state, props }) => [state.value, props.propIn],
|
|
799
|
-
// compute derived state from state & props (use short aliases)
|
|
800
|
-
get: ({ s, p }) => s.value * p.propIn,
|
|
801
|
-
},
|
|
802
|
-
},
|
|
803
|
-
}
|
|
804
|
-
);
|
|
821
|
+
// Simple presentation components
|
|
822
|
+
// Use memo to prevent re-render if another part of the state has changed than "title"
|
|
823
|
+
const Title = memo(function Title(props: { title: string }) {
|
|
824
|
+
return <div>{props.text}</div>;
|
|
825
|
+
});
|
|
805
826
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
827
|
+
const Subtitle = memo(function Subtitle(props: { subtitle: string }) {
|
|
828
|
+
return <div>{props.subtitle}</div>;
|
|
829
|
+
});
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### Local Store
|
|
833
|
+
|
|
834
|
+
```typescript
|
|
835
|
+
import React, { memo } from 'react';
|
|
836
|
+
import { useLocalStore } from 'state-decorator';
|
|
837
|
+
|
|
838
|
+
// Declare typings & actions as above
|
|
839
|
+
|
|
840
|
+
// Create and bind to react component
|
|
841
|
+
// Each time the store state changes, a re-render is done.
|
|
842
|
+
export function Container(props: Props) {
|
|
843
|
+
const { state, actions } = useLocalStore(getInitialState, userAppActions, props);
|
|
844
|
+
return (
|
|
845
|
+
<div>
|
|
846
|
+
<Title text={state.title} />
|
|
847
|
+
<Subtitle text={state.subtitle} />
|
|
848
|
+
</div>
|
|
849
|
+
);
|
|
810
850
|
}
|
|
851
|
+
|
|
852
|
+
// Simple presentation components
|
|
853
|
+
// Use memo to prevent re-render if another part of the state has changed than "title"
|
|
854
|
+
const Title = memo(function Title(props: { title: string }) {
|
|
855
|
+
return <div>{props.title}</div>;
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
const Subtitle = memo(function Subtitle(props: { subtitle: string }) {
|
|
859
|
+
return <div>{props.subtitle}</div>;
|
|
860
|
+
});
|
|
811
861
|
```
|
|
812
862
|
|
|
813
|
-
##
|
|
863
|
+
## State slices
|
|
814
864
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
865
|
+
Using React props implies useless re-renders and optionally memoization.
|
|
866
|
+
|
|
867
|
+
To prevent this, React provides [context](https://en.reactjs.org/docs/context.html) hooks.
|
|
868
|
+
|
|
869
|
+
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.
|
|
870
|
+
|
|
871
|
+
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.
|
|
872
|
+
|
|
873
|
+
[](https://codesandbox.io/s/slices-v6-eg471?file=/src/SliceView.tsx)
|
|
874
|
+
|
|
875
|
+
## Global Store
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
// Declare typings & actions as above
|
|
879
|
+
|
|
880
|
+
// Create a store in a dedicated file, named GlobalStore.ts for example
|
|
881
|
+
export const store = createStore(getInitialState, userAppActions);
|
|
882
|
+
|
|
883
|
+
//----------------------
|
|
884
|
+
|
|
885
|
+
import React, { memo } from 'react';
|
|
886
|
+
import { useStoreSlice } from 'state-decorator';
|
|
887
|
+
import store from './GlobalStore';
|
|
888
|
+
|
|
889
|
+
export function Container(props: Props) {
|
|
890
|
+
return (
|
|
891
|
+
<div>
|
|
892
|
+
<Title />
|
|
893
|
+
<Subtitle />
|
|
894
|
+
</div>
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function Title() {
|
|
899
|
+
// extracts the "title" slice
|
|
900
|
+
// component is refresh only if title is changed in the store
|
|
901
|
+
const { title } = useStoreSlice(store, ['title']);
|
|
902
|
+
return <div>{title}</div>;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function Subtitle(props: { subtitle: string }) {
|
|
906
|
+
const { subtitle } = useStoreSlice(store, ['subtitle']);
|
|
907
|
+
return <div>{subtitle}</div>;
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
## Local Store
|
|
912
|
+
|
|
913
|
+
```typescript
|
|
914
|
+
// ------- stores/MyStore.ts --------
|
|
915
|
+
|
|
916
|
+
import React from 'react';
|
|
917
|
+
import { useLocalStore, useStoreContextSlice } from 'state-decorator';
|
|
918
|
+
|
|
919
|
+
// Declare typings & actions as above
|
|
920
|
+
|
|
921
|
+
// Create a store context
|
|
922
|
+
|
|
923
|
+
type StoreContextProps = StoreApi<State, Actions, Props>;
|
|
924
|
+
|
|
925
|
+
export const StoreContext = createContext<StoreContextProps>(null);
|
|
926
|
+
|
|
927
|
+
// The StoreContext will allow to access to the store
|
|
928
|
+
// But this context is NOT refreshed if the store state is changed
|
|
929
|
+
export function StoreContextProvider(p: { children: any; propIn: string }) {
|
|
930
|
+
const { children, ...props } = p;
|
|
931
|
+
const store = useLocalStore(getInitialState, actionsImpl, props);
|
|
932
|
+
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// In a container file ------------------------
|
|
936
|
+
|
|
937
|
+
import { StoreContextProvider } from 'stores/MyStore';
|
|
938
|
+
|
|
939
|
+
// If Container is destroyed store is destroyed
|
|
940
|
+
export function Container(props: Props) {
|
|
941
|
+
return (
|
|
942
|
+
<StoreContextProvider {...props}>
|
|
943
|
+
<SubComponent />
|
|
944
|
+
<SubComponent2 />
|
|
945
|
+
</StoreContextProvider>
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Component deeper in the component tree...
|
|
950
|
+
|
|
951
|
+
import { StoreContext } from 'stores/MyStore';
|
|
952
|
+
|
|
953
|
+
function Title() {
|
|
954
|
+
// extracts the "title" slice
|
|
955
|
+
// component is refresh only if title is changed in the store
|
|
956
|
+
const { title } = useStoreContextSlice(StoreContext, ['title']);
|
|
957
|
+
|
|
958
|
+
// same as
|
|
959
|
+
// const store = useContext(StoreContext)
|
|
960
|
+
// const { title } = useStoreSlice(store, ['title']);
|
|
961
|
+
|
|
962
|
+
return <div>{title}</div>;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function Subtitle(props: { subtitle: string }) {
|
|
966
|
+
const { subtitle } = useStoreContextSlice(StoreContext, ['subtitle']);
|
|
967
|
+
return <div>{subtitle}</div>;
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
# Global configuration
|
|
972
|
+
|
|
973
|
+
Overrides configuration to set properties that will be used by all stores.
|
|
974
|
+
|
|
975
|
+
```typescript
|
|
976
|
+
// all parameters are optional
|
|
977
|
+
setGlobalConfig({
|
|
978
|
+
// Used to clone state and props when managing optimistic conflicting actions.
|
|
979
|
+
clone: defaultCloneFunc,
|
|
980
|
+
// Compare to objets and returns if they are equal (slices).
|
|
981
|
+
comparator: shallow,
|
|
982
|
+
// Callback function to handle asychronous actions rejected promise (error reporting).
|
|
983
|
+
asyncErrorHandler: () => {},
|
|
984
|
+
// Tests if the error will trigger a retry of the action or will fail directly (retry promises)
|
|
985
|
+
retryOnErrorFunction: (error: Error) => error instanceof TypeError,
|
|
986
|
+
// Notification function on successful asynchronous action (if success message is set on action)
|
|
987
|
+
notifySuccess: undefined,
|
|
988
|
+
// Notification function on failed asynchronous action (if error message is set on action)
|
|
989
|
+
notifyError: undefined,
|
|
990
|
+
// Notification function injected in side effects action context (success and error) to notify warning
|
|
991
|
+
notifyWarning: undefined,
|
|
992
|
+
// 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_.
|
|
993
|
+
getErrorMessage: undefined,
|
|
994
|
+
});
|
|
995
|
+
```
|
|
819
996
|
|
|
820
997
|
# Debug actions
|
|
821
998
|
|
|
@@ -900,11 +1077,11 @@ function Container(props: Props) {
|
|
|
900
1077
|
- a new store is created each time state or props are set.
|
|
901
1078
|
- it allows to share mock store across tests.
|
|
902
1079
|
- The API is chainable: functions are returning a mock store, excepting test functions.
|
|
903
|
-
- On a mock store, we can test the store internals after initialiation, after inbound props have changed, after actions are called.
|
|
1080
|
+
- 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.
|
|
904
1081
|
|
|
905
1082
|
## Getting started
|
|
906
1083
|
|
|
907
|
-
1. Create and setup a mock store (_createMockFromStore_)
|
|
1084
|
+
1. Create and setup a mock store (_createMockFromStore_, _createMockStore_)
|
|
908
1085
|
2. Test store after initialization (_onInit_)
|
|
909
1086
|
3. Test store after props changes (_onPropsChange_)
|
|
910
1087
|
4. Test each action:
|