Skip to content

Commit 3a8f5d0

Browse files
committed
feat: add way to track fetchable actions without state field
1 parent 681e4b4 commit 3a8f5d0

File tree

7 files changed

+198
-46
lines changed

7 files changed

+198
-46
lines changed

example/typed/src/components/order/index.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import * as React from 'react';
22
import styled from 'styled-components';
33
import { connect } from 'react-redux';
4-
import { FetchableStatus, FetchableValue } from 'reducktion';
4+
5+
import {
6+
FetchableStatus,
7+
FetchableValue,
8+
FetchableValueSimple,
9+
} from 'reducktion';
510

611
import orderModel from './order.model';
712
import { Order } from './order.types';
813

914
class OrderComp extends React.Component<{
1015
orders: FetchableValue<Order[]>;
1116
fetchOrders: () => any;
17+
saveCreditCardState: FetchableValueSimple;
1218
}> {
1319
render() {
14-
const { orders, fetchOrders } = this.props;
20+
const { orders, fetchOrders, saveCreditCardState } = this.props;
21+
console.log(':::> saveCreditCardState', saveCreditCardState);
1522

1623
return (
1724
<Container>
@@ -22,9 +29,21 @@ class OrderComp extends React.Component<{
2229
<Loading>Loading orders...</Loading>
2330
)}
2431

32+
{saveCreditCardState.status === FetchableStatus.LOADING && (
33+
<Loading>Saving card...</Loading>
34+
)}
35+
36+
{saveCreditCardState.status === FetchableStatus.FAILURE && (
37+
<Loading>Failed to save card!</Loading>
38+
)}
39+
40+
{saveCreditCardState.status === FetchableStatus.SUCCESS && (
41+
<Loading>Saved card!</Loading>
42+
)}
43+
2544
{orders.data.length > 0 && (
2645
<Orders>
27-
{orders.data.map((order) => (
46+
{orders.data.map(order => (
2847
<li key={order.id}>{order.name}</li>
2948
))}
3049
</Orders>
@@ -50,6 +69,9 @@ const Orders = styled.ul`
5069
export default connect(
5170
state => ({
5271
orders: orderModel.selectors.get('orders')(state),
72+
saveCreditCardState: orderModel.selectors.getAction('saveCreditCard')(
73+
state
74+
),
5375
}),
5476
{
5577
fetchOrders: orderModel.actions.fetchOrders,

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface State {
2626
export interface Actions {
2727
fetchOrders: FetchableAction<Order[]>;
2828
fetchPackages: FetchableAction<Package[]>;
29+
saveCreditCard: FetchableAction;
2930
fooAction: (lol: number) => any;
3031
lolAction: (lol: number) => any;
3132
someThunk: (arg: any) => any;
@@ -60,6 +61,7 @@ const model = createModel<State, Actions, Selectors, Deps>({
6061
lolAction: () => ({ ...initialState }),
6162

6263
// Fetchable actions
64+
saveCreditCard: fetchable.action(),
6365
fetchPackages: fetchable.action('packages'),
6466
fetchOrders: fetchable.action('orders', {
6567
// Define custom reducer for different statuses
@@ -102,6 +104,12 @@ const model = createModel<State, Actions, Selectors, Deps>({
102104
function* fetchOrdersSaga(action: any): any {
103105
console.log({ action });
104106
try {
107+
yield put(model.actions.saveCreditCard())
108+
yield sleep(1000);
109+
yield put(model.actions.saveCreditCard.fail('Failed to save card'))
110+
yield sleep(1000);
111+
yield put(model.actions.saveCreditCard.success())
112+
105113
// Select the fetchable value
106114
const x = yield select(model.selectors.getSomethingComplex);
107115
const orders2 = yield select(model.selectors.get('orders'));

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ declare module 'reducktion' {
44
? A
55
: never;
66

7-
interface RootState {
8-
[statePart: string]: any;
9-
}
10-
117
// Provide action keys for auto-complete but allow custom types
128
// that are eg. auto-generated by fetchable action
139
type ActionTypes<Actions> = { [K in keyof Actions]: string } & {
@@ -46,7 +42,7 @@ declare module 'reducktion' {
4642
value: <T>(val: T) => FetchableValue<T>;
4743
action: <State, K extends keyof State>(
4844
// Only allow state fields for fetchable values
49-
stateField: FetchableValue extends State[K] ? K : never,
45+
stateField?: FetchableValue extends State[K] ? K : never,
5046
customReducers?: Partial<FetchableReducers<State>>
5147
) => FetchableReducers<State>;
5248
noop: () => NoopReducer;
@@ -67,7 +63,7 @@ declare module 'reducktion' {
6763
? FetchableReducers<State>
6864
: Actions[K] extends Function
6965
? Reducer<State, ArgumentType<Actions[K]>>
70-
: never;
66+
: never
7167
};
7268
reactions?: (
7369
{ initialState, deps }: { initialState: State; deps: Deps }
@@ -94,7 +90,16 @@ declare module 'reducktion' {
9490
selectors: Selectors & {
9591
get: <K extends keyof State>(
9692
stateField: K
97-
) => (state: RootState, ...args: any[]) => Pick<State, K>[K];
93+
) => (
94+
state: { [part: string]: any },
95+
...args: any[]
96+
) => Pick<State, K>[K];
97+
getAction: <K extends keyof Actions>(
98+
actionName: K
99+
) => (
100+
state: { [part: string]: any },
101+
...args: any[]
102+
) => FetchableValueSimple;
98103
};
99104
getSagas: () => [];
100105
getReducer: () => Reducer<any>;
@@ -109,13 +114,18 @@ declare module 'reducktion' {
109114
FAILURE,
110115
}
111116

117+
export interface FetchableValueSimple {
118+
error: any;
119+
status: FetchableStatus;
120+
}
121+
112122
export interface FetchableValue<Data = any> {
113123
data: Data;
114124
error: any;
115125
status: FetchableStatus;
116126
}
117127

118-
export interface FetchableAction<SuccessData> extends ActionFunc {
128+
export interface FetchableAction<SuccessData = any> extends ActionFunc {
119129
init: ActionFunc;
120130
fail: ActionFunc;
121131
success: ActionFunc<SuccessData>;

reducktion.test.js

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import {
55
FetchableStatus,
66
} from './src/reducktion';
77

8-
// TODO: fix tests related to auto-generated selectors!
9-
108
describe('fetchable', () => {
119
it('should create fetchable value', () => {
1210
const f = fetchable.value([]);
@@ -31,6 +29,82 @@ describe('fetchable', () => {
3129
const state = { test: 1, test2: 2 };
3230
expect(reducer(state)).toEqual(state);
3331
});
32+
33+
it('should track fetchable action if no state field is given', () => {
34+
const model = createModel({
35+
name: 'test',
36+
state: {
37+
something: 1,
38+
},
39+
actions: () => ({
40+
testAction: fetchable.action(),
41+
}),
42+
});
43+
44+
const state = { test: { something: 1 } };
45+
const selector = model.selectors.getAction('testAction');
46+
47+
expect(selector(state)).toEqual({
48+
status: FetchableStatus.INITIAL,
49+
error: null,
50+
});
51+
52+
state.test.actions = {
53+
testAction: { status: FetchableStatus.LOADING, error: null },
54+
};
55+
56+
expect(selector(state)).toEqual({
57+
status: FetchableStatus.LOADING,
58+
error: null,
59+
});
60+
61+
state.test.actions = {
62+
testAction: { status: FetchableStatus.FAILURE, error: 'err' },
63+
};
64+
65+
expect(selector(state)).toEqual({
66+
status: FetchableStatus.FAILURE,
67+
error: 'err',
68+
});
69+
70+
state.test.actions = {};
71+
72+
expect(selector(state)).toEqual({
73+
status: FetchableStatus.INITIAL,
74+
error: null,
75+
});
76+
});
77+
78+
it('should NOT track fetchable action when state field is given', () => {
79+
const model = createModel({
80+
name: 'test',
81+
state: {
82+
something: fetchable.value(1),
83+
},
84+
actions: () => ({
85+
testAction: fetchable.action('something'),
86+
}),
87+
});
88+
89+
const state = {
90+
test: {
91+
something: { data: 1, status: FetchableStatus.INITIAL, error: null },
92+
},
93+
};
94+
95+
// NOTE: `getAction` returns a simple fetchable value even if `testAction`
96+
// is not actually tracked
97+
expect(model.selectors.getAction('testAction')(state)).toEqual({
98+
status: FetchableStatus.INITIAL,
99+
error: null,
100+
});
101+
102+
expect(model.selectors.get('something')(state)).toEqual({
103+
data: 1,
104+
status: FetchableStatus.INITIAL,
105+
error: null,
106+
});
107+
});
34108
});
35109

36110
describe('createModel', () => {
@@ -238,18 +312,6 @@ describe('createModel', () => {
238312
expect(model.selectors.get('orders')(state)).toEqual(orders);
239313
});
240314

241-
it('should throw if fetchable action has no success field', () => {
242-
expect(() => {
243-
createModel({
244-
name: 'test',
245-
state: {
246-
orders: fetchable.value([]),
247-
},
248-
actions: () => ({ testAction: fetchable.action() }),
249-
});
250-
}).toThrowError(/you must provide the name of the field/i);
251-
});
252-
253315
it('should not require initial data for fetchable', () => {
254316
const model = createModel({
255317
name: 'test',
@@ -320,7 +382,7 @@ describe('initModels', () => {
320382
'toggleNotifications',
321383
]);
322384
expect(Object.keys(settings.selectors).sort()).toEqual(
323-
['get', 'getCustomSelector'].sort()
385+
['get', 'getAction', 'getCustomSelector'].sort()
324386
);
325387
expect(settings.getSagas()).toEqual([]);
326388
});

src/helpers.js

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,40 @@ export const handleThunks = (thunks, dependencies) =>
6464
return acc;
6565
}, {});
6666

67-
const createFetchableReducers = ({ types, successField, overrides }) => {
68-
if (!successField) {
69-
throw Error(
70-
'You must provide the name of the field that is used for success payload'
71-
);
72-
}
67+
const createSimpleFetchableReducers = ({ types, actionName }) => ({
68+
[types.loading]: state => ({
69+
...state,
70+
actions: {
71+
...state.actions,
72+
[actionName]: {
73+
status: FETCHABLE_STATUS.LOADING,
74+
error: null,
75+
},
76+
},
77+
}),
78+
[types.success]: state => ({
79+
...state,
80+
actions: {
81+
...state.actions,
82+
[actionName]: {
83+
status: FETCHABLE_STATUS.SUCCESS,
84+
error: null,
85+
},
86+
},
87+
}),
88+
[types.failure]: (state, action) => ({
89+
...state,
90+
actions: {
91+
...state.actions,
92+
[actionName]: {
93+
status: FETCHABLE_STATUS.FAILURE,
94+
error: action.payload,
95+
},
96+
},
97+
}),
98+
});
7399

100+
const createFetchableReducers = ({ types, successField, overrides }) => {
74101
const defaultReducers = {
75102
[types.loading]: state => ({
76103
...state,
@@ -133,11 +160,14 @@ export function handleFetchableAction(args, actionName, modelName) {
133160

134161
// User can either provide only reducer field name for success case
135162
// or reducer overrides for `loading` / `success` / `failure` cases
136-
const reducers = createFetchableReducers({
137-
types: t,
138-
successField: args.length > 0 ? args[0] : null,
139-
overrides: args.length > 1 ? args[1] : {},
140-
});
163+
const successField = args.length > 0 ? args[0] : null;
164+
const overrides = args.length > 1 ? args[1] : {};
165+
166+
// If no success field is provided -> create reducers for for tracking
167+
// the status and error of the fetchable action
168+
const reducers = successField
169+
? createFetchableReducers({ types: t, successField, overrides })
170+
: createSimpleFetchableReducers({ types: t, actionName });
141171

142172
// Return types that are inlined to the other types instead of accessing them
143173
// via `types.fetchSomething.success` you access them normally

0 commit comments

Comments
 (0)