Skip to content

Commit

Permalink
feat: enable composing selectors and require model selector types
Browse files Browse the repository at this point in the history
  • Loading branch information
Temzasse committed Jan 18, 2019
1 parent 66e4be1 commit 29ba577
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 62 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,17 @@ export default createModel({
isLoading: false,
hasError: false,
},
selectors: ({ name }) => ({
selectors: ({ name, selectors }) => ({
getOrders: state => state[name].orders,
getIsLoading: state => state[name].isLoading,
getHasError: state => state[name].hasError,
// Composing selectors is also easy
getComposed: state => {
const isLoading = selectors.getIsLoading(state);
const orders = selectors.getOrders(state);
if (isLoading || orders.length === 0) return [];
return orders.filter(o => o.something !== 'amazing');
},
}),
actions: () => ({
fetchOrders: state => ({ ... }),
Expand Down
5 changes: 5 additions & 0 deletions example/typed/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions example/typed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"redux-logger": "^3.0.6",
"redux-saga": "^0.16.2",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"styled-components": "^4.1.3",
"typescript": "^3.2.2"
},
Expand Down
41 changes: 30 additions & 11 deletions example/typed/src/components/order/order.model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { takeEvery, put, select } from 'redux-saga/effects';
import { ThunkDispatch } from 'redux-thunk';
import { Action } from 'redux';
import { createSelector } from 'reselect';

import {
createModel,
Expand All @@ -12,7 +13,9 @@ import {
import { sleep } from '../../helpers';
import { UserModel } from '../user/user.model';
import { Order, Package } from './order.types';
import { InitialState } from '../types';

// #region types
export interface State {
foo: number;
bar: string;
Expand All @@ -28,11 +31,18 @@ export interface Actions {
someThunk: (arg: any) => any;
}

interface Selectors {
getFoo: (state: InitialState) => number;
getOrdersData: (state: InitialState) => Order[];
getSomethingComplex: (state: InitialState) => string;
}

interface Deps {
user: UserModel;
}
// #endregion

const model = createModel<State, Actions, Deps>({
const model = createModel<State, Actions, Selectors, Deps>({
name: 'order',
inject: ['user'],
state: {
Expand Down Expand Up @@ -61,12 +71,18 @@ const model = createModel<State, Actions, Deps>({
// TODO: fix...
someThunk: state => state,
}),
selectors: ({ name }) => ({
getFoo: state => state[name].foo,
getOrdersCustom: state => state[name].orders.data,
getBarYeyd: state => {
const x = 'yey';
return `${state[name].bar}-${x}`;
selectors: ({ selectors }) => ({
getFoo: state => state.order.foo,
getOrdersData: state => state.order.orders.data,
getSomethingComplex: state => {
const sel = createSelector(
[selectors.getFoo, selectors.getOrdersData],
(foo, data) => {
if (data.length === 0) return 'No orders';
return `${foo}-${data[0].id}`;
}
);
return sel(state);
},
}),
thunks: {
Expand All @@ -87,12 +103,14 @@ function* fetchOrdersSaga(action: any): any {
console.log({ action });
try {
// Select the fetchable value
const orders1 = yield select(model.selectors.get('orders'));
const x = yield select(model.selectors.getSomethingComplex);
const orders2 = yield select(model.selectors.get('orders'));

console.log({ x, orders2 });

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

console.log({ orders1 });
console.log({ foo });

// Fake API call delay
yield sleep(2000);
Expand All @@ -108,6 +126,7 @@ function* fetchOrdersSaga(action: any): any {
])
);
} catch (error) {
console.log('FAIL!', error);
yield put(model.actions.fetchOrders.fail('Could not load orders!'));
}
}
Expand Down
13 changes: 9 additions & 4 deletions example/typed/src/components/settings/settings.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Action } from 'redux';

import { UserModel } from '../user/user.model';
import { OrderModel } from '../order/order.model';
import { InitialState } from '../types';

interface Actions {
resetSettings: () => any;
Expand All @@ -13,27 +14,31 @@ interface Actions {
testThunk: () => any;
}

interface State {
export interface State {
notificationsEnabled: boolean;
gpsEnabled: boolean;
darkModeEnabled: boolean;
}

interface Selectors {
getThemeMode: (state: InitialState) => 'light' | 'dark';
}

interface Deps {
user: UserModel;
order: OrderModel;
}

const model = createModel<State, Actions, Deps>({
const model = createModel<State, Actions, Selectors, Deps>({
name: 'settings',
inject: ['user', 'order'],
state: {
notificationsEnabled: false,
gpsEnabled: false,
darkModeEnabled: false,
},
selectors: ({ name }) => ({
getThemeMode: state => (state[name].darkModeEnabled ? 'dark' : 'light'),
selectors: () => ({
getThemeMode: state => (state.settings.darkModeEnabled ? 'dark' : 'light'),
}),
actions: ({ initialState }) => ({
resetSettings: () => ({ ...initialState }),
Expand Down
9 changes: 9 additions & 0 deletions example/typed/src/components/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { State as OrderState } from './order/order.model';
import { State as UserState } from './user/user.model';
import { State as SettingsState } from './settings/settings.model';

export interface InitialState {
user: UserState;
order: OrderState;
settings: SettingsState;
}
30 changes: 11 additions & 19 deletions example/typed/typings/reducktion/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ declare module 'reducktion' {
[statePart: string]: StatePart;
}

type Selector<State> = (state: RootState<State>, ...args: any[]) => any;

// Provide action keys for auto-complete but allow custom types
// that are eg. auto-generated by fetchable action
type ActionTypes<Actions> = { [K in keyof Actions]: string } & {
Expand Down Expand Up @@ -52,16 +50,12 @@ declare module 'reducktion' {
) => FetchableReducers<State>;
}

interface Dependencies {
[depName: string]: Model<any, any>;
}

// TODO:
// Figure out how to show proper error
// if given action is not in keyof Actions
interface ModelDefinition<State, Actions, Deps> {
interface ModelDefinition<State, Actions, Selectors, Deps> {
name: string;
inject?: [keyof Deps];
inject?: (keyof Deps)[];
state: State;
actions: (
{ initialState }: { initialState: State }
Expand All @@ -79,10 +73,8 @@ declare module 'reducktion' {
[depType: string]: Reducer<State>;
};
selectors?: (
{ name }: { name: string }
) => {
[selectorName: string]: Selector<State>;
};
{ name, selectors }: { name: string; selectors: Selectors }
) => Selectors;
sagas?: (
{ types, deps }: { types: ActionTypes<Actions>; deps: Deps }
) => any[];
Expand All @@ -92,12 +84,12 @@ declare module 'reducktion' {
};
}

interface Model<State, Actions> {
interface Model<State, Actions, Selectors> {
name: string;
initialState: State;
types: ActionTypes<Actions>;
actions: Actions;
selectors: {
selectors: Selectors & {
get: <K extends keyof State>(
stateField: K
) => (state: RootState<State>, ...args: any[]) => Pick<State, K>[K];
Expand Down Expand Up @@ -129,18 +121,18 @@ declare module 'reducktion' {

export const fetchable: Fetchable;

export function createModel<State, Actions, Deps = Dependencies>(
df: ModelDefinition<State, Actions, Deps>
): Model<State, Actions>;
export function createModel<State, Actions, Selectors = {}, Deps = {}>(
df: ModelDefinition<State, Actions, Selectors, Deps>
): Model<State, Actions, Selectors>;

export function initModels(
models: Model<any, any>[]
models: Model<any, any, any>[]
): {
allReducers: {
[x: string]: Reducer<any>;
};
allSagas: any[];
} & {
[modelName: string]: Model<any, any>;
[modelName: string]: Model<any, any, any>;
};
}
32 changes: 32 additions & 0 deletions reducktion.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,38 @@ describe('createModel', () => {
model.selectors.get('wrong')({ test: { field: 1 } });
}).toThrowError(/select a non-existent field 'wrong'/i);
});

it('should use custom selector', () => {
const model = createModel({
name: 'test',
state: { field: 1 },
actions: () => ({ testAction: state => ({ ...state }) }),
selectors: ({ name }) => ({
getField: state => state[name].field,
}),
});
const testState = { test: { field: 1 } };
expect(model.selectors.getField(testState)).toBe(1);
});

it('should be able to compose custom selectors', () => {
const model = createModel({
name: 'test',
state: { field1: 1, field2: 2 },
actions: () => ({ testAction: state => ({ ...state }) }),
selectors: ({ name, selectors }) => ({
getField1: state => state[name].field1,
getField2: state => state[name].field2,
getComposed: state => {
const field1 = selectors.getField1(state);
const field2 = selectors.getField2(state);
return `${field1}${field2}`;
},
}),
});
const testState = { test: { field1: 1, field2: 2 } };
expect(model.selectors.getComposed(testState)).toBe('12');
});
});

describe('sagas', () => {
Expand Down
30 changes: 11 additions & 19 deletions src/reducktion.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ interface RootState<StatePart> {
[statePart: string]: StatePart;
}

type Selector<State> = (state: RootState<State>, ...args: any[]) => any;

// Provide action keys for auto-complete but allow custom types
// that are eg. auto-generated by fetchable action
type ActionTypes<Actions> = { [K in keyof Actions]: string } & {
Expand Down Expand Up @@ -51,16 +49,12 @@ interface Fetchable {
) => FetchableReducers<State>;
}

interface Dependencies {
[depName: string]: Model<any, any>;
}

// TODO:
// Figure out how to show proper error
// if given action is not in keyof Actions
interface ModelDefinition<State, Actions, Deps> {
interface ModelDefinition<State, Actions, Selectors, Deps> {
name: string;
inject?: [keyof Deps];
inject?: (keyof Deps)[];
state: State;
actions: (
{ initialState }: { initialState: State }
Expand All @@ -78,10 +72,8 @@ interface ModelDefinition<State, Actions, Deps> {
[depType: string]: Reducer<State>;
};
selectors?: (
{ name }: { name: string }
) => {
[selectorName: string]: Selector<State>;
};
{ name, selectors }: { name: string; selectors: Selectors }
) => Selectors;
sagas?: (
{ types, deps }: { types: ActionTypes<Actions>; deps: Deps }
) => any[];
Expand All @@ -91,12 +83,12 @@ interface ModelDefinition<State, Actions, Deps> {
};
}

interface Model<State, Actions> {
interface Model<State, Actions, Selectors> {
name: string;
initialState: State;
types: ActionTypes<Actions>;
actions: Actions;
selectors: {
selectors: Selectors & {
get: <K extends keyof State>(
stateField: K
) => (state: RootState<State>, ...args: any[]) => Pick<State, K>[K];
Expand Down Expand Up @@ -128,17 +120,17 @@ export interface FetchableAction<SuccessData> extends ActionFunc {

export const fetchable: Fetchable;

export function createModel<State, Actions, Deps = Dependencies>(
df: ModelDefinition<State, Actions, Deps>
): Model<State, Actions>;
export function createModel<State, Actions, Selectors = {}, Deps = {}>(
df: ModelDefinition<State, Actions, Selectors, Deps>
): Model<State, Actions, Selectors>;

export function initModels(
models: Model<any, any>[]
models: Model<any, any, any>[]
): {
allReducers: {
[x: string]: Reducer<any>;
};
allSagas: any[];
} & {
[modelName: string]: Model<any, any>;
[modelName: string]: Model<any, any, any>;
};
Loading

0 comments on commit 29ba577

Please sign in to comment.