state-decorator 6.8.0-beta.1 → 6.8.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 +227 -376
- package/impl.d.ts +7 -1
- package/impl.es.js +1 -1
- package/impl.es.js.map +1 -1
- package/impl.js +1 -1
- package/impl.js.map +1 -1
- package/index.d.ts +22 -55
- package/index.es.js +1 -1
- package/index.es.js.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/middlewares.es.js +1 -1
- package/middlewares.es.js.map +1 -1
- package/middlewares.js +1 -1
- package/middlewares.js.map +1 -1
- package/package.json +1 -1
- package/test.d.ts +13 -3
- package/test.es.js +1 -1
- package/test.es.js.map +1 -1
- package/test.js +1 -1
- package/test.js.map +1 -1
- package/types.d.ts +9 -0
- package/v5.es.js +1 -1
package/README.md
CHANGED
|
@@ -9,13 +9,14 @@ The StateDecorator is a set of Reacts hook that manages a complex component stat
|
|
|
9
9
|
# Features
|
|
10
10
|
|
|
11
11
|
- Deterministic state changes (clear separation of effects and side effects)
|
|
12
|
-
- Ease asynchronous actions state changes (loading states, success & error handlers, parallel actions management, optimistic updates...)
|
|
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 (uses pure functions, utility functions provided)
|
|
15
15
|
- Easily update state from or react to props changes
|
|
16
|
-
- Ease debugging (trace state changes
|
|
16
|
+
- Ease debugging (trace state changes)
|
|
17
17
|
- Improve code conciseness (no boiler plate code)
|
|
18
|
-
-
|
|
18
|
+
- Enforce separation of container components and presentation components.
|
|
19
|
+
- Strongly typed
|
|
19
20
|
|
|
20
21
|
# 🏎️ V6: The Store
|
|
21
22
|
|
|
@@ -138,12 +139,12 @@ export function App(props: Props) => {
|
|
|
138
139
|
|
|
139
140
|
# React hooks
|
|
140
141
|
|
|
141
|
-
| Hook
|
|
142
|
-
|
|
|
143
|
-
| useLocalStore
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
| useStoreSlice
|
|
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 |
|
|
147
148
|
|
|
148
149
|
# Initial state
|
|
149
150
|
|
|
@@ -315,15 +316,34 @@ const actions: StoreActions<State, Actions> = {
|
|
|
315
316
|
The loading state of asynchronous actions is automatically computed.
|
|
316
317
|
|
|
317
318
|
```typescript
|
|
318
|
-
const { isLoading, loading } = useLocalStore(getInitialState, actionsImpl);
|
|
319
|
+
const { isLoading, loading, loadingMap } = useLocalStore(getInitialState, actionsImpl);
|
|
319
320
|
|
|
320
321
|
// loading is set to true is at least one asynchronous action is ongoing.
|
|
322
|
+
// loadingMap is action name to boolean or undefined map
|
|
321
323
|
|
|
322
324
|
// isLoading is a function that accepts 1+ arguments
|
|
323
325
|
// It returns true, if at least one of the specified actions is loading.
|
|
324
326
|
const isOneLoading = isLoading('action1', 'action2', ['parallelAction', 'promiseId']);
|
|
325
327
|
```
|
|
326
328
|
|
|
329
|
+
## Error state
|
|
330
|
+
|
|
331
|
+
The last error of each asynchronous actions is available on the errorMap.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
const { errorMap } = useLocalStore(getInitialState, actionsImpl);
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if (errorMap.action1)) {
|
|
338
|
+
// show retry action
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (errorMap.action1 instanceof MyCustomError ){
|
|
342
|
+
// show retry action with custom error handling
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
|
|
327
347
|
## Success / error notifications
|
|
328
348
|
|
|
329
349
|
A notification function can be called when the asynchronous action succeeded or failed.
|
|
@@ -491,235 +511,64 @@ When a hook that creates / binds a store is mounted, the **onMount** option is c
|
|
|
491
511
|
```typescript
|
|
492
512
|
import { createStore } from 'state-decorator';
|
|
493
513
|
|
|
494
|
-
// global store
|
|
495
514
|
const store = createStore(getInitialState, actionsImpl, {
|
|
496
515
|
onMount: ({ a }) => {
|
|
497
516
|
a.loadList;
|
|
498
517
|
},
|
|
499
518
|
});
|
|
500
519
|
|
|
501
|
-
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
```typescript
|
|
505
|
-
import { createStore } from 'state-decorator';
|
|
506
|
-
|
|
507
|
-
// local store
|
|
520
|
+
// onMount is called when this component is mounted
|
|
508
521
|
function Container(p: ContainerProps) {
|
|
509
|
-
|
|
510
|
-
onMount: ({ a }) => {
|
|
511
|
-
a.loadList;
|
|
512
|
-
},
|
|
513
|
-
});
|
|
522
|
+
useBindStore(store, p);
|
|
514
523
|
}
|
|
515
524
|
```
|
|
516
525
|
|
|
517
526
|
If an initial action is launched each time a property changes, consider using the **onMount** flag of the **onPropsChange** entry.
|
|
518
527
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
There are two ways to share state and actions:
|
|
522
|
-
|
|
523
|
-
- Using regular props.
|
|
524
|
-
- Using state slices.
|
|
525
|
-
|
|
526
|
-
There are two types of stores:
|
|
527
|
-
|
|
528
|
-
- Global store:
|
|
529
|
-
|
|
530
|
-
- store is created as an instance and it is exported from the JS module.
|
|
531
|
-
- it can be imported in any other JS module.
|
|
532
|
-
- store lifespan is equal to the application one.
|
|
533
|
-
|
|
534
|
-
- Local store:
|
|
535
|
-
- store is created by a hook and is bound to a React component (each instance of this component owns its own store).
|
|
536
|
-
- action and props can be shared using props or sharing the store itself (see example below)
|
|
537
|
-
- store is destroyed when its React component is unmounted.
|
|
538
|
-
|
|
539
|
-
## Using props
|
|
540
|
-
|
|
541
|
-
- Bind a React component to make store state / actions available on React component.
|
|
542
|
-
- Any change in the store (state, loading actions, ...) will trigger a refresh of the bound React component.
|
|
543
|
-
- Pass state / actions using regular React props.
|
|
544
|
-
- Use React.memo to prevent unecessary React re-renders if needed.
|
|
545
|
-
|
|
546
|
-
### Global store
|
|
547
|
-
|
|
548
|
-
```typescript
|
|
549
|
-
// Declare typings & actions as above
|
|
550
|
-
|
|
551
|
-
// Create a store in a dedicated file, named GlobalStore.ts for example
|
|
552
|
-
export const store = createStore(getInitialState, userAppActions);
|
|
553
|
-
|
|
554
|
-
//----------------------
|
|
555
|
-
|
|
556
|
-
// In another file
|
|
557
|
-
import React, { memo } from 'react';
|
|
558
|
-
import { useStore } from 'state-decorator';
|
|
559
|
-
import store from './GlobalStore';
|
|
560
|
-
|
|
561
|
-
// Bind to react component
|
|
562
|
-
// Each time the store state changes, a re-render is done.
|
|
563
|
-
export function Container(props: Props) {
|
|
564
|
-
const { state, actions } = useStore(store, props);
|
|
565
|
-
return (
|
|
566
|
-
<div>
|
|
567
|
-
<Title text={state.title} />
|
|
568
|
-
<Subtitle text={state.subtitle} />
|
|
569
|
-
</div>
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Simple presentation components
|
|
574
|
-
// Use memo to prevent re-render if another part of the state has changed than "title"
|
|
575
|
-
const Title = memo(function Title(props: { title: string }) {
|
|
576
|
-
return <div>{props.text}</div>;
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
const Subtitle = memo(function Subtitle(props: { subtitle: string }) {
|
|
580
|
-
return <div>{props.subtitle}</div>;
|
|
581
|
-
});
|
|
582
|
-
```
|
|
528
|
+
## State sharing and slices
|
|
583
529
|
|
|
584
|
-
|
|
530
|
+
- Declare a store and bind it to a component using _useStore_ or _useBindStore_ hooks.
|
|
531
|
+
- Deeper in the tree component, use _useStoreSlice_ to get a store slice.
|
|
532
|
+
- Component using slices will be refreshed only if their store slice changes.
|
|
585
533
|
|
|
586
534
|
```typescript
|
|
587
|
-
import React, { memo } from 'react';
|
|
588
|
-
import { useLocalStore } from 'state-decorator';
|
|
589
|
-
|
|
590
|
-
// Declare typings & actions as above
|
|
591
|
-
|
|
592
|
-
// Create and bind to react component
|
|
593
|
-
// Each time the store state changes, a re-render is done.
|
|
594
|
-
export function Container(props: Props) {
|
|
595
|
-
const { state, actions } = useLocalStore(getInitialState, userAppActions, props);
|
|
596
|
-
return (
|
|
597
|
-
<div>
|
|
598
|
-
<Title text={state.title} />
|
|
599
|
-
<Subtitle text={state.subtitle} />
|
|
600
|
-
</div>
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Simple presentation components
|
|
605
|
-
// Use memo to prevent re-render if another part of the state has changed than "title"
|
|
606
|
-
const Title = memo(function Title(props: { title: string }) {
|
|
607
|
-
return <div>{props.title}</div>;
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
const Subtitle = memo(function Subtitle(props: { subtitle: string }) {
|
|
611
|
-
return <div>{props.subtitle}</div>;
|
|
612
|
-
});
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
## State slices
|
|
616
|
-
|
|
617
|
-
Using React props implies useless re-renders and optionally memoization.
|
|
618
|
-
|
|
619
|
-
To prevent this, React provides [context](https://en.reactjs.org/docs/context.html) hooks.
|
|
620
|
-
|
|
621
|
-
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.
|
|
622
|
-
|
|
623
|
-
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 slice has changed.
|
|
624
|
-
|
|
625
|
-
[](https://codesandbox.io/s/slices-v6-eg471?file=/src/SliceView.tsx)
|
|
626
|
-
|
|
627
|
-
## Global Store
|
|
628
|
-
|
|
629
|
-
```typescript
|
|
630
|
-
// Declare typings & actions as above
|
|
631
|
-
|
|
632
|
-
// Create a store in a dedicated file, named GlobalStore.ts for example
|
|
633
|
-
export const store = createStore(getInitialState, userAppActions);
|
|
634
|
-
|
|
635
|
-
//----------------------
|
|
636
|
-
|
|
637
|
-
import React, { memo } from 'react';
|
|
638
|
-
import { useStoreSlice } from 'state-decorator';
|
|
639
|
-
import store from './GlobalStore';
|
|
640
|
-
|
|
641
|
-
export function Container(props: Props) {
|
|
642
|
-
return (
|
|
643
|
-
<div>
|
|
644
|
-
<Title />
|
|
645
|
-
<Subtitle />
|
|
646
|
-
</div>
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function Title() {
|
|
651
|
-
// extracts the "title" slice
|
|
652
|
-
// component is refresh only if title is changed in the store
|
|
653
|
-
const { title } = useStoreSlice(store, ['title']);
|
|
654
|
-
return <div>{title}</div>;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function Subtitle(props: { subtitle: string }) {
|
|
658
|
-
const { subtitle } = useStoreSlice(store, ['subtitle']);
|
|
659
|
-
return <div>{subtitle}</div>;
|
|
660
|
-
}
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
## Local Store
|
|
664
|
-
|
|
665
|
-
```typescript
|
|
666
|
-
// ------- stores/MyStore.ts --------
|
|
667
|
-
|
|
668
535
|
import React from 'react';
|
|
669
|
-
import {
|
|
536
|
+
import { useStore, useStoreSlice, StoreActions } from 'state-decorator';
|
|
670
537
|
|
|
671
538
|
// Declare typings & actions as above
|
|
672
539
|
|
|
673
|
-
// Create a store
|
|
674
|
-
|
|
675
|
-
type StoreContextProps = StoreApi<State, Actions, Props>;
|
|
540
|
+
// Create a store
|
|
676
541
|
|
|
677
|
-
export const
|
|
542
|
+
export const store = createStore(getInitialState, userAppActions);
|
|
678
543
|
|
|
679
|
-
//
|
|
680
|
-
// But this context is NOT refreshed if the store state is changed
|
|
681
|
-
export function StoreContextProvider(p: { children: any; propIn: string }) {
|
|
682
|
-
const { children, ...props } = p;
|
|
683
|
-
const store = useGetLocalStore(getInitialState, actionsImpl, props);
|
|
684
|
-
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
|
|
685
|
-
}
|
|
544
|
+
// Bind to react component
|
|
686
545
|
|
|
687
|
-
|
|
546
|
+
export function Container(prop: Props) {
|
|
547
|
+
const { state, actions } = useStore(store, props);
|
|
688
548
|
|
|
689
|
-
|
|
549
|
+
// or
|
|
550
|
+
// useBindStore(store, props);
|
|
551
|
+
// if you are not interested in getting the state here
|
|
690
552
|
|
|
691
|
-
|
|
692
|
-
export function Container(props: Props) {
|
|
693
|
-
return (
|
|
694
|
-
<StoreContextProvider {...props}>
|
|
695
|
-
<SubComponent />
|
|
696
|
-
<SubComponent2 />
|
|
697
|
-
</StoreContextProvider>
|
|
698
|
-
);
|
|
553
|
+
return <div />;
|
|
699
554
|
}
|
|
700
555
|
|
|
701
|
-
//
|
|
702
|
-
|
|
703
|
-
import { StoreContext } from 'stores/MyStore';
|
|
704
|
-
|
|
705
|
-
function Title() {
|
|
706
|
-
// extracts the "title" slice
|
|
707
|
-
// component is refresh only if title is changed in the store
|
|
708
|
-
const { title } = useStoreContextSlice(StoreContext, ['title']);
|
|
556
|
+
// Components deeper in the component tree will be refreshed if, and only if,
|
|
557
|
+
// slice is changed (here: text property)
|
|
709
558
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return <div>{title}</div>;
|
|
559
|
+
export function SubComponent2() {
|
|
560
|
+
const s = useStoreSlice(store, ['text']);
|
|
561
|
+
return <div>{s.text}</div>;
|
|
715
562
|
}
|
|
716
563
|
|
|
717
|
-
function
|
|
718
|
-
const
|
|
719
|
-
return <div>{
|
|
564
|
+
export function SubComponent() {
|
|
565
|
+
const s = useStoreSlice(store, (s) => ({ text: s.text }));
|
|
566
|
+
return <div>{s.text}</div>;
|
|
720
567
|
}
|
|
721
568
|
```
|
|
722
569
|
|
|
570
|
+
[](https://codesandbox.io/s/slices-v6-eg471?file=/src/SliceView.tsx)
|
|
571
|
+
|
|
723
572
|
# Global configuration
|
|
724
573
|
|
|
725
574
|
Overrides configuration to set properties that will be used by all stores.
|
|
@@ -1101,172 +950,6 @@ describe('Todo', () => {
|
|
|
1101
950
|
});
|
|
1102
951
|
});
|
|
1103
952
|
|
|
1104
|
-
it('onSetNewTitle', async () => {
|
|
1105
|
-
// always return a promise whatever if the action is asynchronous or not
|
|
1106
|
-
const { state, prevState } = await store.getAction('onSetNewTitle').call('new Title');
|
|
1107
|
-
|
|
1108
|
-
// previous state is available for comparison
|
|
1109
|
-
expect(prevState.newTitle).toEqual('');
|
|
1110
|
-
expect(state.newTitle).toEqual('new Title');
|
|
1111
|
-
});
|
|
1112
|
-
|
|
1113
|
-
it('onCreate', async () => {
|
|
1114
|
-
const {
|
|
1115
|
-
state: { todoIds, todoMap, todos },
|
|
1116
|
-
} = await store
|
|
1117
|
-
// set partial state and return new store, source store is untouched
|
|
1118
|
-
.setPartialState({
|
|
1119
|
-
newTitle: 'new todo',
|
|
1120
|
-
})
|
|
1121
|
-
// get a mock action that can be shared too !
|
|
1122
|
-
.getAction('onCreate')
|
|
1123
|
-
// Invokes action. It has no side effect on state of mock action
|
|
1124
|
-
.call();
|
|
1125
|
-
|
|
1126
|
-
// assert state
|
|
1127
|
-
expect(todoIds).toEqual(['id', 'id2', 'id3']);
|
|
1128
|
-
expect(todoMap).toEqual({
|
|
1129
|
-
id: {
|
|
1130
|
-
id: 'id',
|
|
1131
|
-
completed: true,
|
|
1132
|
-
title: 'todo',
|
|
1133
|
-
},
|
|
1134
|
-
id2: {
|
|
1135
|
-
id: 'id2',
|
|
1136
|
-
completed: false,
|
|
1137
|
-
title: 'todo 2',
|
|
1138
|
-
},
|
|
1139
|
-
id3: {
|
|
1140
|
-
id: 'id3',
|
|
1141
|
-
completed: false,
|
|
1142
|
-
title: 'new todo',
|
|
1143
|
-
},
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
// assert derived state
|
|
1147
|
-
//
|
|
1148
|
-
// export const todoOptions: StoreOptions<State, Actions, Props, DerivedState> = {
|
|
1149
|
-
// derivedState: {
|
|
1150
|
-
// todos: {
|
|
1151
|
-
// getDeps: ({ s }) => [s.todoIds, s.todoMap],
|
|
1152
|
-
// get: ({ s }) => s.todoIds.map((id) => s.todoMap[id]),
|
|
1153
|
-
// },
|
|
1154
|
-
// },
|
|
1155
|
-
// };
|
|
1156
|
-
|
|
1157
|
-
expect(todos).toEqual([
|
|
1158
|
-
{
|
|
1159
|
-
id: 'id',
|
|
1160
|
-
completed: true,
|
|
1161
|
-
title: 'todo',
|
|
1162
|
-
},
|
|
1163
|
-
{
|
|
1164
|
-
id: 'id2',
|
|
1165
|
-
completed: false,
|
|
1166
|
-
title: 'todo 2',
|
|
1167
|
-
},
|
|
1168
|
-
{
|
|
1169
|
-
id: 'id3',
|
|
1170
|
-
completed: false,
|
|
1171
|
-
title: 'new todo',
|
|
1172
|
-
},
|
|
1173
|
-
]);
|
|
1174
|
-
});
|
|
1175
|
-
|
|
1176
|
-
describe('onToggle', () => {
|
|
1177
|
-
// this mock action is shared in two tests but they can run in parallel
|
|
1178
|
-
const onToggle = store.getAction('onToggle');
|
|
1179
|
-
|
|
1180
|
-
it('toggle false => true', async () => {
|
|
1181
|
-
// arguments of call() depend on the Store action signature, use auto completion!
|
|
1182
|
-
const {
|
|
1183
|
-
state: { todoIds, todoMap },
|
|
1184
|
-
actions,
|
|
1185
|
-
} = await onToggle.call('id2');
|
|
1186
|
-
|
|
1187
|
-
expect(todoIds).toEqual(['id', 'id2']);
|
|
1188
|
-
expect(todoMap).toEqual({
|
|
1189
|
-
id: {
|
|
1190
|
-
id: 'id',
|
|
1191
|
-
completed: true,
|
|
1192
|
-
title: 'todo',
|
|
1193
|
-
},
|
|
1194
|
-
id2: {
|
|
1195
|
-
id: 'id2',
|
|
1196
|
-
completed: true,
|
|
1197
|
-
title: 'todo 2',
|
|
1198
|
-
},
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
// if there are some side effects calling store actions, you can test them
|
|
1202
|
-
// as mock actions are injected instead of real actions.
|
|
1203
|
-
expect(actions.updateRemoteList).toHaveBeenCalled();
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
it('toggle true => false', async () => {
|
|
1207
|
-
const {
|
|
1208
|
-
state: { todoIds, todoMap },
|
|
1209
|
-
actions,
|
|
1210
|
-
} = await onToggle.call('id');
|
|
1211
|
-
|
|
1212
|
-
expect(todoIds).toEqual(['id', 'id2']);
|
|
1213
|
-
expect(todoMap).toEqual({
|
|
1214
|
-
id: {
|
|
1215
|
-
id: 'id',
|
|
1216
|
-
completed: false,
|
|
1217
|
-
title: 'todo',
|
|
1218
|
-
},
|
|
1219
|
-
id2: {
|
|
1220
|
-
id: 'id2',
|
|
1221
|
-
completed: false,
|
|
1222
|
-
title: 'todo 2',
|
|
1223
|
-
},
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
// if there are some side effects calling store actions, you can test them
|
|
1227
|
-
// as mock actions are injected instead of real actions.
|
|
1228
|
-
expect(actions.updateRemoteList).toHaveBeenCalled();
|
|
1229
|
-
});
|
|
1230
|
-
});
|
|
1231
|
-
|
|
1232
|
-
// Another store / action
|
|
1233
|
-
describe('loadRemoteList', () => {
|
|
1234
|
-
// action is shared
|
|
1235
|
-
const action = store.getAction('loadRemoteList');
|
|
1236
|
-
|
|
1237
|
-
it('success', async () => {
|
|
1238
|
-
const { state } = await action
|
|
1239
|
-
// override default promise result
|
|
1240
|
-
.promiseResolves([
|
|
1241
|
-
{
|
|
1242
|
-
id: 'item1',
|
|
1243
|
-
title: 'item title',
|
|
1244
|
-
completed: true,
|
|
1245
|
-
},
|
|
1246
|
-
])
|
|
1247
|
-
.call();
|
|
1248
|
-
|
|
1249
|
-
expect(state.error).toBeFalsy();
|
|
1250
|
-
expect(state.todoIds).toEqual(['item1']);
|
|
1251
|
-
expect(state.todoMap).toEqual({
|
|
1252
|
-
item1: {
|
|
1253
|
-
id: 'item1',
|
|
1254
|
-
title: 'item title',
|
|
1255
|
-
completed: true,
|
|
1256
|
-
},
|
|
1257
|
-
});
|
|
1258
|
-
});
|
|
1259
|
-
|
|
1260
|
-
it('error', async () => {
|
|
1261
|
-
const { state } = await action
|
|
1262
|
-
// override default promise result with error
|
|
1263
|
-
.promiseRejects(new Error('boom'))
|
|
1264
|
-
.call();
|
|
1265
|
-
|
|
1266
|
-
expect(state.error).toBeTruthy();
|
|
1267
|
-
});
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1270
953
|
// Test effects and side effect when props are changing
|
|
1271
954
|
//
|
|
1272
955
|
// Here:
|
|
@@ -1316,6 +999,174 @@ describe('Todo', () => {
|
|
|
1316
999
|
expect(actions.loadRemoteList).not.toHaveBeenCalled();
|
|
1317
1000
|
});
|
|
1318
1001
|
});
|
|
1002
|
+
|
|
1003
|
+
describe('actions', () => {
|
|
1004
|
+
it('onSetNewTitle', async () => {
|
|
1005
|
+
// always return a promise whatever if the action is asynchronous or not
|
|
1006
|
+
const { state, prevState } = await store.getAction('onSetNewTitle').call('new Title');
|
|
1007
|
+
|
|
1008
|
+
// previous state is available for comparison
|
|
1009
|
+
expect(prevState.newTitle).toEqual('');
|
|
1010
|
+
expect(state.newTitle).toEqual('new Title');
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('onCreate', async () => {
|
|
1014
|
+
const {
|
|
1015
|
+
state: { todoIds, todoMap, todos },
|
|
1016
|
+
} = await store
|
|
1017
|
+
// set partial state and return new store, source store is untouched
|
|
1018
|
+
.setPartialState({
|
|
1019
|
+
newTitle: 'new todo',
|
|
1020
|
+
})
|
|
1021
|
+
// get a mock action that can be shared too !
|
|
1022
|
+
.getAction('onCreate')
|
|
1023
|
+
// Invokes action. It has no side effect on state of mock action
|
|
1024
|
+
.call();
|
|
1025
|
+
|
|
1026
|
+
// assert state
|
|
1027
|
+
expect(todoIds).toEqual(['id', 'id2', 'id3']);
|
|
1028
|
+
expect(todoMap).toEqual({
|
|
1029
|
+
id: {
|
|
1030
|
+
id: 'id',
|
|
1031
|
+
completed: true,
|
|
1032
|
+
title: 'todo',
|
|
1033
|
+
},
|
|
1034
|
+
id2: {
|
|
1035
|
+
id: 'id2',
|
|
1036
|
+
completed: false,
|
|
1037
|
+
title: 'todo 2',
|
|
1038
|
+
},
|
|
1039
|
+
id3: {
|
|
1040
|
+
id: 'id3',
|
|
1041
|
+
completed: false,
|
|
1042
|
+
title: 'new todo',
|
|
1043
|
+
},
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// assert derived state
|
|
1047
|
+
//
|
|
1048
|
+
// export const todoOptions: StoreOptions<State, Actions, Props, DerivedState> = {
|
|
1049
|
+
// derivedState: {
|
|
1050
|
+
// todos: {
|
|
1051
|
+
// getDeps: ({ s }) => [s.todoIds, s.todoMap],
|
|
1052
|
+
// get: ({ s }) => s.todoIds.map((id) => s.todoMap[id]),
|
|
1053
|
+
// },
|
|
1054
|
+
// },
|
|
1055
|
+
// };
|
|
1056
|
+
|
|
1057
|
+
expect(todos).toEqual([
|
|
1058
|
+
{
|
|
1059
|
+
id: 'id',
|
|
1060
|
+
completed: true,
|
|
1061
|
+
title: 'todo',
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
id: 'id2',
|
|
1065
|
+
completed: false,
|
|
1066
|
+
title: 'todo 2',
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
id: 'id3',
|
|
1070
|
+
completed: false,
|
|
1071
|
+
title: 'new todo',
|
|
1072
|
+
},
|
|
1073
|
+
]);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
describe('onToggle', () => {
|
|
1077
|
+
// this mock action is shared in two tests but they can run in parallel
|
|
1078
|
+
const onToggle = store.getAction('onToggle');
|
|
1079
|
+
|
|
1080
|
+
it('toggle false => true', async () => {
|
|
1081
|
+
// arguments of call() depend on the Store action signature, use auto completion!
|
|
1082
|
+
const {
|
|
1083
|
+
state: { todoIds, todoMap },
|
|
1084
|
+
actions,
|
|
1085
|
+
} = await onToggle.call('id2');
|
|
1086
|
+
|
|
1087
|
+
expect(todoIds).toEqual(['id', 'id2']);
|
|
1088
|
+
expect(todoMap).toEqual({
|
|
1089
|
+
id: {
|
|
1090
|
+
id: 'id',
|
|
1091
|
+
completed: true,
|
|
1092
|
+
title: 'todo',
|
|
1093
|
+
},
|
|
1094
|
+
id2: {
|
|
1095
|
+
id: 'id2',
|
|
1096
|
+
completed: true,
|
|
1097
|
+
title: 'todo 2',
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
// if there are some side effects calling store actions, you can test them
|
|
1102
|
+
// as mock actions are injected instead of real actions.
|
|
1103
|
+
expect(actions.updateRemoteList).toHaveBeenCalled();
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it('toggle true => false', async () => {
|
|
1107
|
+
const {
|
|
1108
|
+
state: { todoIds, todoMap },
|
|
1109
|
+
actions,
|
|
1110
|
+
} = await onToggle.call('id');
|
|
1111
|
+
|
|
1112
|
+
expect(todoIds).toEqual(['id', 'id2']);
|
|
1113
|
+
expect(todoMap).toEqual({
|
|
1114
|
+
id: {
|
|
1115
|
+
id: 'id',
|
|
1116
|
+
completed: false,
|
|
1117
|
+
title: 'todo',
|
|
1118
|
+
},
|
|
1119
|
+
id2: {
|
|
1120
|
+
id: 'id2',
|
|
1121
|
+
completed: false,
|
|
1122
|
+
title: 'todo 2',
|
|
1123
|
+
},
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
// if there are some side effects calling store actions, you can test them
|
|
1127
|
+
// as mock actions are injected instead of real actions.
|
|
1128
|
+
expect(actions.updateRemoteList).toHaveBeenCalled();
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
// Another store / action
|
|
1133
|
+
describe('loadRemoteList', () => {
|
|
1134
|
+
// action is shared
|
|
1135
|
+
const action = store.getAction('loadRemoteList');
|
|
1136
|
+
|
|
1137
|
+
it('success', async () => {
|
|
1138
|
+
const { state } = await action
|
|
1139
|
+
// override default promise result
|
|
1140
|
+
.promiseResolves([
|
|
1141
|
+
{
|
|
1142
|
+
id: 'item1',
|
|
1143
|
+
title: 'item title',
|
|
1144
|
+
completed: true,
|
|
1145
|
+
},
|
|
1146
|
+
])
|
|
1147
|
+
.call();
|
|
1148
|
+
|
|
1149
|
+
expect(state.error).toBeFalsy();
|
|
1150
|
+
expect(state.todoIds).toEqual(['item1']);
|
|
1151
|
+
expect(state.todoMap).toEqual({
|
|
1152
|
+
item1: {
|
|
1153
|
+
id: 'item1',
|
|
1154
|
+
title: 'item title',
|
|
1155
|
+
completed: true,
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
it('error', async () => {
|
|
1161
|
+
const { state } = await action
|
|
1162
|
+
// override default promise result with error
|
|
1163
|
+
.promiseRejects(new Error('boom'))
|
|
1164
|
+
.call();
|
|
1165
|
+
|
|
1166
|
+
expect(state.error).toBeTruthy();
|
|
1167
|
+
});
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1319
1170
|
});
|
|
1320
1171
|
```
|
|
1321
1172
|
|
|
@@ -1440,9 +1291,9 @@ TypeError: Cannot set property 'crash' of null
|
|
|
1440
1291
|
### MockStore
|
|
1441
1292
|
|
|
1442
1293
|
| Function | Arguments | Returns | Description |
|
|
1443
|
-
| --------------- | ------------------------------------ | ------------------- | ------------------------------------------------------------------------------------------- |
|
|
1294
|
+
| --------------- | ------------------------------------ | ------------------- | ------------------------------------------------------------------------------------------- |
|
|
1444
1295
|
| getAction | actionName: keyof A | MockStoreAction | Get a mock action instance |
|
|
1445
|
-
| onInit | props?: Partial<P> | MockResult | Test store initialization (onPropsChanged marked onMount + onMount) |
|
|
1296
|
+
| onInit | props?: Partial<P> | MockResult | Test store initialization (onPropsChanged marked onMount + onMount) |
|
|
1446
1297
|
| onPropsChange | props: P, init?: boolean | MockResult | Test prop changes. Does not change internal props. Init allows to test onMount prop changes |
|
|
1447
1298
|
| setPartialProps | props: Partial<P> | new mock store | Create a new store with the merge of current props and partial props |
|
|
1448
1299
|
| setPartialState | state: Partial<S> | new mock store | Create a new store with the merge of current state and partial state |
|