Skip to content

Commit 29ba577

Browse files
committed
feat: enable composing selectors and require model selector types
1 parent 66e4be1 commit 29ba577

File tree

10 files changed

+126
-62
lines changed

10 files changed

+126
-62
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,17 @@ export default createModel({
129129
isLoading: false,
130130
hasError: false,
131131
},
132-
selectors: ({ name }) => ({
132+
selectors: ({ name, selectors }) => ({
133133
getOrders: state => state[name].orders,
134134
getIsLoading: state => state[name].isLoading,
135135
getHasError: state => state[name].hasError,
136+
// Composing selectors is also easy
137+
getComposed: state => {
138+
const isLoading = selectors.getIsLoading(state);
139+
const orders = selectors.getOrders(state);
140+
if (isLoading || orders.length === 0) return [];
141+
return orders.filter(o => o.something !== 'amazing');
142+
},
136143
}),
137144
actions: () => ({
138145
fetchOrders: state => ({ ... }),

example/typed/package-lock.json

Lines changed: 5 additions & 0 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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"redux-logger": "^3.0.6",
1919
"redux-saga": "^0.16.2",
2020
"redux-thunk": "^2.3.0",
21+
"reselect": "^4.0.0",
2122
"styled-components": "^4.1.3",
2223
"typescript": "^3.2.2"
2324
},

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

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { takeEvery, put, select } from 'redux-saga/effects';
22
import { ThunkDispatch } from 'redux-thunk';
33
import { Action } from 'redux';
4+
import { createSelector } from 'reselect';
45

56
import {
67
createModel,
@@ -12,7 +13,9 @@ import {
1213
import { sleep } from '../../helpers';
1314
import { UserModel } from '../user/user.model';
1415
import { Order, Package } from './order.types';
16+
import { InitialState } from '../types';
1517

18+
// #region types
1619
export interface State {
1720
foo: number;
1821
bar: string;
@@ -28,11 +31,18 @@ export interface Actions {
2831
someThunk: (arg: any) => any;
2932
}
3033

34+
interface Selectors {
35+
getFoo: (state: InitialState) => number;
36+
getOrdersData: (state: InitialState) => Order[];
37+
getSomethingComplex: (state: InitialState) => string;
38+
}
39+
3140
interface Deps {
3241
user: UserModel;
3342
}
43+
// #endregion
3444

35-
const model = createModel<State, Actions, Deps>({
45+
const model = createModel<State, Actions, Selectors, Deps>({
3646
name: 'order',
3747
inject: ['user'],
3848
state: {
@@ -61,12 +71,18 @@ const model = createModel<State, Actions, Deps>({
6171
// TODO: fix...
6272
someThunk: state => state,
6373
}),
64-
selectors: ({ name }) => ({
65-
getFoo: state => state[name].foo,
66-
getOrdersCustom: state => state[name].orders.data,
67-
getBarYeyd: state => {
68-
const x = 'yey';
69-
return `${state[name].bar}-${x}`;
74+
selectors: ({ selectors }) => ({
75+
getFoo: state => state.order.foo,
76+
getOrdersData: state => state.order.orders.data,
77+
getSomethingComplex: state => {
78+
const sel = createSelector(
79+
[selectors.getFoo, selectors.getOrdersData],
80+
(foo, data) => {
81+
if (data.length === 0) return 'No orders';
82+
return `${foo}-${data[0].id}`;
83+
}
84+
);
85+
return sel(state);
7086
},
7187
}),
7288
thunks: {
@@ -87,12 +103,14 @@ function* fetchOrdersSaga(action: any): any {
87103
console.log({ action });
88104
try {
89105
// Select the fetchable value
90-
const orders1 = yield select(model.selectors.get('orders'));
106+
const x = yield select(model.selectors.getSomethingComplex);
107+
const orders2 = yield select(model.selectors.get('orders'));
108+
109+
console.log({ x, orders2 });
91110

92-
// Or use a custom selector to get the data field directly
93-
// const orders2: Order[] = yield select(model.selectors);
111+
const foo = yield select(model.selectors.getFoo);
94112

95-
console.log({ orders1 });
113+
console.log({ foo });
96114

97115
// Fake API call delay
98116
yield sleep(2000);
@@ -108,6 +126,7 @@ function* fetchOrdersSaga(action: any): any {
108126
])
109127
);
110128
} catch (error) {
129+
console.log('FAIL!', error);
111130
yield put(model.actions.fetchOrders.fail('Could not load orders!'));
112131
}
113132
}

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Action } from 'redux';
44

55
import { UserModel } from '../user/user.model';
66
import { OrderModel } from '../order/order.model';
7+
import { InitialState } from '../types';
78

89
interface Actions {
910
resetSettings: () => any;
@@ -13,27 +14,31 @@ interface Actions {
1314
testThunk: () => any;
1415
}
1516

16-
interface State {
17+
export interface State {
1718
notificationsEnabled: boolean;
1819
gpsEnabled: boolean;
1920
darkModeEnabled: boolean;
2021
}
2122

23+
interface Selectors {
24+
getThemeMode: (state: InitialState) => 'light' | 'dark';
25+
}
26+
2227
interface Deps {
2328
user: UserModel;
2429
order: OrderModel;
2530
}
2631

27-
const model = createModel<State, Actions, Deps>({
32+
const model = createModel<State, Actions, Selectors, Deps>({
2833
name: 'settings',
2934
inject: ['user', 'order'],
3035
state: {
3136
notificationsEnabled: false,
3237
gpsEnabled: false,
3338
darkModeEnabled: false,
3439
},
35-
selectors: ({ name }) => ({
36-
getThemeMode: state => (state[name].darkModeEnabled ? 'dark' : 'light'),
40+
selectors: () => ({
41+
getThemeMode: state => (state.settings.darkModeEnabled ? 'dark' : 'light'),
3742
}),
3843
actions: ({ initialState }) => ({
3944
resetSettings: () => ({ ...initialState }),
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { State as OrderState } from './order/order.model';
2+
import { State as UserState } from './user/user.model';
3+
import { State as SettingsState } from './settings/settings.model';
4+
5+
export interface InitialState {
6+
user: UserState;
7+
order: OrderState;
8+
settings: SettingsState;
9+
}

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

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ declare module 'reducktion' {
99
[statePart: string]: StatePart;
1010
}
1111

12-
type Selector<State> = (state: RootState<State>, ...args: any[]) => any;
13-
1412
// Provide action keys for auto-complete but allow custom types
1513
// that are eg. auto-generated by fetchable action
1614
type ActionTypes<Actions> = { [K in keyof Actions]: string } & {
@@ -52,16 +50,12 @@ declare module 'reducktion' {
5250
) => FetchableReducers<State>;
5351
}
5452

55-
interface Dependencies {
56-
[depName: string]: Model<any, any>;
57-
}
58-
5953
// TODO:
6054
// Figure out how to show proper error
6155
// if given action is not in keyof Actions
62-
interface ModelDefinition<State, Actions, Deps> {
56+
interface ModelDefinition<State, Actions, Selectors, Deps> {
6357
name: string;
64-
inject?: [keyof Deps];
58+
inject?: (keyof Deps)[];
6559
state: State;
6660
actions: (
6761
{ initialState }: { initialState: State }
@@ -79,10 +73,8 @@ declare module 'reducktion' {
7973
[depType: string]: Reducer<State>;
8074
};
8175
selectors?: (
82-
{ name }: { name: string }
83-
) => {
84-
[selectorName: string]: Selector<State>;
85-
};
76+
{ name, selectors }: { name: string; selectors: Selectors }
77+
) => Selectors;
8678
sagas?: (
8779
{ types, deps }: { types: ActionTypes<Actions>; deps: Deps }
8880
) => any[];
@@ -92,12 +84,12 @@ declare module 'reducktion' {
9284
};
9385
}
9486

95-
interface Model<State, Actions> {
87+
interface Model<State, Actions, Selectors> {
9688
name: string;
9789
initialState: State;
9890
types: ActionTypes<Actions>;
9991
actions: Actions;
100-
selectors: {
92+
selectors: Selectors & {
10193
get: <K extends keyof State>(
10294
stateField: K
10395
) => (state: RootState<State>, ...args: any[]) => Pick<State, K>[K];
@@ -129,18 +121,18 @@ declare module 'reducktion' {
129121

130122
export const fetchable: Fetchable;
131123

132-
export function createModel<State, Actions, Deps = Dependencies>(
133-
df: ModelDefinition<State, Actions, Deps>
134-
): Model<State, Actions>;
124+
export function createModel<State, Actions, Selectors = {}, Deps = {}>(
125+
df: ModelDefinition<State, Actions, Selectors, Deps>
126+
): Model<State, Actions, Selectors>;
135127

136128
export function initModels(
137-
models: Model<any, any>[]
129+
models: Model<any, any, any>[]
138130
): {
139131
allReducers: {
140132
[x: string]: Reducer<any>;
141133
};
142134
allSagas: any[];
143135
} & {
144-
[modelName: string]: Model<any, any>;
136+
[modelName: string]: Model<any, any, any>;
145137
};
146138
}

reducktion.test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,38 @@ describe('createModel', () => {
118118
model.selectors.get('wrong')({ test: { field: 1 } });
119119
}).toThrowError(/select a non-existent field 'wrong'/i);
120120
});
121+
122+
it('should use custom selector', () => {
123+
const model = createModel({
124+
name: 'test',
125+
state: { field: 1 },
126+
actions: () => ({ testAction: state => ({ ...state }) }),
127+
selectors: ({ name }) => ({
128+
getField: state => state[name].field,
129+
}),
130+
});
131+
const testState = { test: { field: 1 } };
132+
expect(model.selectors.getField(testState)).toBe(1);
133+
});
134+
135+
it('should be able to compose custom selectors', () => {
136+
const model = createModel({
137+
name: 'test',
138+
state: { field1: 1, field2: 2 },
139+
actions: () => ({ testAction: state => ({ ...state }) }),
140+
selectors: ({ name, selectors }) => ({
141+
getField1: state => state[name].field1,
142+
getField2: state => state[name].field2,
143+
getComposed: state => {
144+
const field1 = selectors.getField1(state);
145+
const field2 = selectors.getField2(state);
146+
return `${field1}${field2}`;
147+
},
148+
}),
149+
});
150+
const testState = { test: { field1: 1, field2: 2 } };
151+
expect(model.selectors.getComposed(testState)).toBe('12');
152+
});
121153
});
122154

123155
describe('sagas', () => {

src/reducktion.d.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ interface RootState<StatePart> {
88
[statePart: string]: StatePart;
99
}
1010

11-
type Selector<State> = (state: RootState<State>, ...args: any[]) => any;
12-
1311
// Provide action keys for auto-complete but allow custom types
1412
// that are eg. auto-generated by fetchable action
1513
type ActionTypes<Actions> = { [K in keyof Actions]: string } & {
@@ -51,16 +49,12 @@ interface Fetchable {
5149
) => FetchableReducers<State>;
5250
}
5351

54-
interface Dependencies {
55-
[depName: string]: Model<any, any>;
56-
}
57-
5852
// TODO:
5953
// Figure out how to show proper error
6054
// if given action is not in keyof Actions
61-
interface ModelDefinition<State, Actions, Deps> {
55+
interface ModelDefinition<State, Actions, Selectors, Deps> {
6256
name: string;
63-
inject?: [keyof Deps];
57+
inject?: (keyof Deps)[];
6458
state: State;
6559
actions: (
6660
{ initialState }: { initialState: State }
@@ -78,10 +72,8 @@ interface ModelDefinition<State, Actions, Deps> {
7872
[depType: string]: Reducer<State>;
7973
};
8074
selectors?: (
81-
{ name }: { name: string }
82-
) => {
83-
[selectorName: string]: Selector<State>;
84-
};
75+
{ name, selectors }: { name: string; selectors: Selectors }
76+
) => Selectors;
8577
sagas?: (
8678
{ types, deps }: { types: ActionTypes<Actions>; deps: Deps }
8779
) => any[];
@@ -91,12 +83,12 @@ interface ModelDefinition<State, Actions, Deps> {
9183
};
9284
}
9385

94-
interface Model<State, Actions> {
86+
interface Model<State, Actions, Selectors> {
9587
name: string;
9688
initialState: State;
9789
types: ActionTypes<Actions>;
9890
actions: Actions;
99-
selectors: {
91+
selectors: Selectors & {
10092
get: <K extends keyof State>(
10193
stateField: K
10294
) => (state: RootState<State>, ...args: any[]) => Pick<State, K>[K];
@@ -128,17 +120,17 @@ export interface FetchableAction<SuccessData> extends ActionFunc {
128120

129121
export const fetchable: Fetchable;
130122

131-
export function createModel<State, Actions, Deps = Dependencies>(
132-
df: ModelDefinition<State, Actions, Deps>
133-
): Model<State, Actions>;
123+
export function createModel<State, Actions, Selectors = {}, Deps = {}>(
124+
df: ModelDefinition<State, Actions, Selectors, Deps>
125+
): Model<State, Actions, Selectors>;
134126

135127
export function initModels(
136-
models: Model<any, any>[]
128+
models: Model<any, any, any>[]
137129
): {
138130
allReducers: {
139131
[x: string]: Reducer<any>;
140132
};
141133
allSagas: any[];
142134
} & {
143-
[modelName: string]: Model<any, any>;
135+
[modelName: string]: Model<any, any, any>;
144136
};

0 commit comments

Comments
 (0)