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 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 (test framework provided)
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, dev tools)
16
+ - Ease debugging (trace state changes)
17
17
  - Improve code conciseness (no boiler plate code)
18
- - Strongly typed (Typescript)
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 | Purpose | Return | Component refreshed on store change | Store is destroyed on unmount |
142
- | ---------------- | --------------------------------------------------- | -------------- | ----------------------------------- | ----------------------------- |
143
- | useLocalStore | Create a store, binds it to the react component. | State, actions | Y | Y |
144
- | useGetLocalStore | Create a store, binds it to the react component. | Store | N | Y |
145
- | useStore | Binds a store instance to the react component. | State, actions | Y | N |
146
- | useStoreSlice | Binds a store slice to a react component (sharing). | Slice | If slice has changed only | N |
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
- store.init(initialProps);
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
- const store = useGetLocalStore(getInitialState, actionsImpl, p, {
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
- # Global and Local Stores VS State sharing
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
- ### Local Store
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
- [![Edit Slice](https://codesandbox.io/static/img/play-codesandbox.svg)](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 { useGetLocalStore, useStoreContextSlice } from 'state-decorator';
536
+ import { useStore, useStoreSlice, StoreActions } from 'state-decorator';
670
537
 
671
538
  // Declare typings & actions as above
672
539
 
673
- // Create a store context
674
-
675
- type StoreContextProps = StoreApi<State, Actions, Props>;
540
+ // Create a store
676
541
 
677
- export const StoreContext = createContext<StoreContextProps>(null);
542
+ export const store = createStore(getInitialState, userAppActions);
678
543
 
679
- // The StoreContext will allow to access to the store
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
- // In a container file ------------------------
546
+ export function Container(prop: Props) {
547
+ const { state, actions } = useStore(store, props);
688
548
 
689
- import { StoreContextProvider } from 'stores/MyStore';
549
+ // or
550
+ // useBindStore(store, props);
551
+ // if you are not interested in getting the state here
690
552
 
691
- // If Container is destroyed store is destroyed
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
- // Component deeper in the component tree...
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
- // same as
711
- // const store = useContext(StoreContext)
712
- // const { title } = useStoreSlice(store, ['title']);
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 Subtitle(props: { subtitle: string }) {
718
- const { subtitle } = useStoreContextSlice(StoreContext, ['subtitle']);
719
- return <div>{subtitle}</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
+ [![Edit Slice](https://codesandbox.io/static/img/play-codesandbox.svg)](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 |