Skip to content

Commit 5e349d6

Browse files
committed
feat: data updater function for fetchable action
1 parent 6778376 commit 5e349d6

File tree

12 files changed

+4206
-2193
lines changed

12 files changed

+4206
-2193
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,31 @@ const model = createModel({
652652
});
653653
```
654654

655+
The default behaviour for `fetchable.success` reducer is to always replace the whole `data` field with the action's payload, so under the hood the updater function is the following:
656+
657+
```js
658+
const updater = (data, action) => action.payload;
659+
```
660+
661+
However, often you don't simply want to replace the data field but insted merge it with the payload somehow. For these use cases it is possible to pass an updater function to the fetchable action as the third parameter. Note that you don't need to pass the reducer definition object if you don't need it - a simple `null` is just fine.
662+
663+
```js
664+
const mergeUpdater = (data, action) => ({ ...data, ...action.payload });
665+
const appendUpdater = (data, action) => [...data, ...action.payload];
666+
667+
const model = createModel({
668+
state: {
669+
ordersById: fetchable.value({}),
670+
otherField: fetchable.value([]),
671+
},
672+
actions: () => ({
673+
fetchOrders: fetchable.action('ordersById', null, mergeUpdater),
674+
fetchOtherField: fetchable.action('otherField', null, appendUpdater),
675+
}),
676+
// ...
677+
});
678+
```
679+
655680
In some cases your actions don't need to update the state in any way and you might just want to listen to the action in your sagas. For these cases Reducktion also provides a helper function `fetchable.noop()` that returns a no-op reducer so the action won't update the state but you can still the action type when setuping your saga watchers.
656681

657682
If you need to access the fetchable action types also in your `reactions` you can do it in the following way:

example/typed/package-lock.json

Lines changed: 3988 additions & 2154 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/typed/package.json

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,29 @@
55
"dependencies": {
66
"@types/jest": "^23.3.10",
77
"@types/node": "^10.12.18",
8-
"@types/react": "^16.7.17",
9-
"@types/react-dom": "^16.0.11",
10-
"@types/react-redux": "^6.0.11",
11-
"@types/redux-logger": "^3.0.6",
12-
"@types/styled-components": "^4.1.4",
13-
"react": "^16.7.0",
14-
"react-dom": "^16.7.0",
15-
"react-redux": "^6.0.0",
16-
"react-scripts": "2.1.1",
8+
"@types/react": "^16.8.8",
9+
"@types/react-dom": "^16.8.3",
10+
"@types/react-redux": "^7.0.5",
11+
"@types/redux-logger": "^3.0.7",
12+
"@types/styled-components": "^4.1.12",
13+
"react": "^16.8.4",
14+
"react-dom": "^16.8.4",
15+
"react-redux": "^6.0.1",
16+
"react-scripts": "2.1.8",
1717
"redux": "^4.0.1",
1818
"redux-logger": "^3.0.6",
19-
"redux-saga": "^0.16.2",
19+
"redux-saga": "^1.0.2",
2020
"redux-thunk": "^2.3.0",
2121
"reselect": "^4.0.0",
2222
"styled-components": "^4.1.3",
23-
"typescript": "^3.2.2"
23+
"typescript": "^3.3.4000"
2424
},
2525
"scripts": {
2626
"start": "react-scripts start",
2727
"build": "react-scripts build",
2828
"test": "react-scripts test",
29-
"eject": "react-scripts eject"
29+
"eject": "react-scripts eject",
30+
"install:clean": "rm -rf node_modules && npm install"
3031
},
3132
"eslintConfig": {
3233
"extends": "react-app"

example/typed/src/components/order/order.model.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
FetchableAction,
1111
} from 'reducktion';
1212

13-
import { sleep } from '../../helpers';
13+
import { sleep, byIdUpdater } from '../../helpers';
1414
import { UserModel } from '../user/user.model';
1515
import { Order, Package } from './order.types';
1616
import { InitialState } from '../types';
@@ -20,7 +20,7 @@ export interface State {
2020
foo: number;
2121
bar: string;
2222
orders: FetchableValue<Order[]>;
23-
packages: FetchableValue<Package[]>;
23+
packagesById: FetchableValue<{ [id: string]: Package }>;
2424
}
2525

2626
export interface Actions {
@@ -50,7 +50,7 @@ const model = createModel<State, Actions, Selectors, Deps>({
5050
foo: 1,
5151
bar: 'lol',
5252
orders: fetchable.value([]),
53-
packages: fetchable.value([]),
53+
packagesById: fetchable.value({}),
5454
},
5555
actions: ({ initialState }) => ({
5656
// Basic actions
@@ -62,7 +62,7 @@ const model = createModel<State, Actions, Selectors, Deps>({
6262

6363
// Fetchable actions
6464
saveCreditCard: fetchable.action(),
65-
fetchPackages: fetchable.action('packages'),
65+
fetchPackages: fetchable.action('packagesById', null, byIdUpdater),
6666
fetchOrders: fetchable.action('orders', {
6767
// Define custom reducer for different statuses
6868
loading: state => ({ ...state, bar: 'loading' }),
@@ -104,13 +104,13 @@ const model = createModel<State, Actions, Selectors, Deps>({
104104
function* fetchOrdersSaga(action: any): any {
105105
console.log({ action });
106106
try {
107-
yield put(model.actions.saveCreditCard())
107+
yield put(model.actions.saveCreditCard());
108108
yield sleep(1000);
109-
yield put(model.actions.saveCreditCard.fail('Failed to save card'))
109+
yield put(model.actions.saveCreditCard.fail('Failed to save card'));
110110
yield sleep(1000);
111-
yield put(model.actions.saveCreditCard.success())
111+
yield put(model.actions.saveCreditCard.success());
112112
yield sleep(1000);
113-
yield put(model.actions.saveCreditCard.clear())
113+
yield put(model.actions.saveCreditCard.clear());
114114

115115
// Select the fetchable value
116116
const x = yield select(model.selectors.getSomethingComplex);
@@ -138,6 +138,16 @@ function* fetchOrdersSaga(action: any): any {
138138

139139
yield sleep(2000);
140140
yield put(model.actions.fetchOrders.clear());
141+
142+
yield put(
143+
model.actions.fetchPackages.success([
144+
{ id: 'pkg1', name: 'Mock package 1' },
145+
{ id: 'pkg2', name: 'Mock package 2' },
146+
{ id: 'pkg3', name: 'Mock package 3' },
147+
{ id: 'pkg4', name: 'Mock package 4' },
148+
])
149+
);
150+
141151
} catch (error) {
142152
console.log('FAIL!', error);
143153
yield put(model.actions.fetchOrders.fail('Could not load orders!'));

example/typed/src/components/order/order.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export interface Order {
77
}
88

99
export interface Package {
10+
id: string;
1011
name: string;
1112
}

example/typed/src/helpers.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
11
export const sleep = (ms = 500): Promise<void> =>
22
new Promise(resolve => setTimeout(resolve, ms));
3+
4+
export interface IdMapped<T> {
5+
[id: string]: T;
6+
}
7+
8+
export const byId = (arr: any[]) => {
9+
return arr.reduce((acc: any, val: any) => {
10+
acc[val.id] = val;
11+
return acc;
12+
}, {});
13+
};
14+
15+
export const byIdUpdater = (data: any, action: any) => ({
16+
...data,
17+
...byId(action.payload),
18+
});

example/typed/tsconfig.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,18 @@
1414
"isolatedModules": true,
1515
"noEmit": true,
1616
"jsx": "preserve",
17-
"typeRoots": [ "./typings", "./node_modules/@types"]
17+
"types": [
18+
"node",
19+
"jest",
20+
"react",
21+
"react-dom",
22+
"react-redux",
23+
"redux-logger",
24+
"styled-components",
25+
"./typings/reducktion"
26+
],
27+
"lib": ["dom", "dom.iterable", "esnext"]
1828
},
1929
"include": ["src"],
20-
"exclude": ["node_modules", "typings"]
30+
"exclude": ["node_modules"]
2131
}

example/typed/typings/reducktion/index.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ declare module 'reducktion' {
4343
action: <State, K extends keyof State>(
4444
// Only allow state fields for fetchable values
4545
stateField?: FetchableValue extends State[K] ? K : never,
46-
customReducers?: Partial<FetchableReducers<State>>
46+
customReducers?: Partial<FetchableReducers<State>> | null,
47+
// TODO: can we provide better types here?
48+
dataUpdater?: (data: any, action: any) => any
4749
) => FetchableReducers<State>;
4850
noop: () => NoopReducer;
4951
}

package-lock.json

Lines changed: 31 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)