Skip to content

Commit 68078a9

Browse files
committed
feat: add fetchable.noop() for no-op reducers
1 parent 29ba577 commit 68078a9

File tree

9 files changed

+67
-39
lines changed

9 files changed

+67
-39
lines changed

README.md

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ _A small helper library for Redux to reduce boilerplate and enforce a more modul
88

99
<br>
1010

11-
- 🦆 **Modular architecture with ducks pattern.**
12-
- 🔮 **Less boilerplate.**
13-
- 💉 **Inject dependencies easily.**
11+
* 🦆 **Modular architecture with ducks pattern.**
12+
* 🔮 **Less boilerplate.**
13+
* 💉 **Inject dependencies easily.**
1414

1515
<br>
1616

@@ -20,18 +20,18 @@ Inspiration: [models: Redux Reducer Bundles](https://github.com/erikras/models-m
2020

2121
---
2222

23-
- [Getting started](#getting-started)
24-
- [Installation](#installation)
25-
- [The Idea](#the-idea)
26-
- [Usage](#usage)
27-
- [Dependency injection](#dependency-injection)
28-
- [Usage with redux-thunk](#usage-with-redux-thunk)
29-
- [Usage with redux-saga](#usage-with-redux-saga)
30-
- [Example with everything](#example-with-everything)
31-
- [Advanced](#advanced)
32-
- [API](#api)
33-
- [Other similar libraries](#other-similar-libraries)
34-
- [Caveats](#caveats)
23+
* [Getting started](#getting-started)
24+
* [Installation](#installation)
25+
* [The Idea](#the-idea)
26+
* [Usage](#usage)
27+
* [Dependency injection](#dependency-injection)
28+
* [Usage with redux-thunk](#usage-with-redux-thunk)
29+
* [Usage with redux-saga](#usage-with-redux-saga)
30+
* [Example with everything](#example-with-everything)
31+
* [Advanced](#advanced)
32+
* [API](#api)
33+
* [Other similar libraries](#other-similar-libraries)
34+
* [Caveats](#caveats)
3535

3636
# reducktion
3737

@@ -653,7 +653,9 @@ const model = createModel({
653653
});
654654
```
655655

656-
If you need to access the API action types eg. in `reactions` or in `sagas` you can do it in the following way:
656+
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.
657+
658+
If you need to access the fetchable action types also in your `reactions` you can do it in the following way:
657659

658660
```js
659661
const model = createModel({
@@ -694,7 +696,7 @@ const fetchablePropType = (dataPropType, errPropType = PropTypes.string) => {
694696
// Using the helper
695697
const propsTypes = {
696698
balanceItems: fetchablePropType(PropTypes.array.isRequired),
697-
}
699+
};
698700
```
699701

700702
## API
@@ -703,8 +705,8 @@ const propsTypes = {
703705
704706
## Other similar libraries
705707

706-
- [Redux Bundler](https://reduxbundler.com/)
707-
- [List of various ducks pattern libs](https://github.com/erikras/models-modular-redux#implementation)
708+
* [Redux Bundler](https://reduxbundler.com/)
709+
* [List of various ducks pattern libs](https://github.com/erikras/models-modular-redux#implementation)
708710

709711
## Caveats
710712

example/typed/src/components/user/LoginForm.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ class LoginForm extends React.Component<Props, State> {
2222
};
2323

2424
handleChange = (e: React.FormEvent<HTMLInputElement>) => {
25-
// TODO: not sure how to type this dynamic state update trick for forms...
26-
// We should probably use separate setter methods for each form field?
2725
const newState = { [e.currentTarget.name]: e.currentTarget.value } as any;
2826
this.setState(newState);
2927
};

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import styled from 'styled-components';
33
import { connect } from 'react-redux';
44
import { FetchableValue } from 'reducktion';
55

6+
import { RootState } from '../../init';
67
import userModel from './user.model';
78
import LoginForm from './LoginForm';
89
import Profile from './Profile';
@@ -59,7 +60,7 @@ const LogoutButton = styled.button`
5960
const { selectors, actions } = userModel;
6061

6162
export default connect(
62-
state => ({
63+
(state: RootState) => ({
6364
isAuthenticated: selectors.get('isAuthenticated')(state),
6465
profile: selectors.get('profile')(state),
6566
}),

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ export interface State {
1515
profile: FetchableValue<Profile | null>;
1616
}
1717

18-
export interface Actions {
18+
interface Actions {
1919
login: FetchableAction<Profile>;
2020
logout: () => any;
21+
deleteUser: () => any;
2122
}
2223

2324
const model = createModel<State, Actions>({
@@ -27,13 +28,17 @@ const model = createModel<State, Actions>({
2728
isAuthenticated: false,
2829
},
2930
actions: ({ initialState }) => ({
31+
deleteUser: fetchable.noop(),
3032
login: fetchable.action('profile', {
3133
success: state => ({ ...state, isAuthenticated: true }),
3234
failure: state => ({ ...state, isAuthenticated: false }),
3335
}),
3436
logout: () => ({ ...initialState }),
3537
}),
36-
sagas: ({ types }) => [takeEvery(types.login, loginSaga)],
38+
sagas: ({ types }) => [
39+
takeEvery(types.login, loginSaga),
40+
takeEvery(types.deleteUser, deleteUserSaga),
41+
],
3742
});
3843

3944
// Saga handlers
@@ -62,6 +67,10 @@ function* loginSaga(action: any): any {
6267
}
6368
}
6469

70+
function* deleteUserSaga(action: any): any {
71+
yield console.log('Do something');
72+
}
73+
6574
export type UserModel = typeof model;
6675

6776
export default model;

example/typed/src/init.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
1+
import { createStore, applyMiddleware, combineReducers, compose, DeepPartial } from 'redux';
22
import { all } from 'redux-saga/effects';
33
import createSagaMiddleware from 'redux-saga';
44
import logger from 'redux-logger';
55
import thunk from 'redux-thunk';
66
import { initModels } from 'reducktion';
77

8-
import orderModel from './components/order/order.model';
9-
import userModel from './components/user/user.model';
8+
import orderModel, { OrderModel } from './components/order/order.model';
9+
import userModel, { UserModel } from './components/user/user.model';
10+
11+
export interface RootState {
12+
order: OrderModel;
13+
user: UserModel;
14+
}
1015

1116
const models = initModels([orderModel, userModel]);
1217
const rootReducer = combineReducers(models.allReducers);
@@ -22,7 +27,7 @@ const enhancer = composeEnhancers(
2227
applyMiddleware(sagaMiddleware, thunk, logger)
2328
);
2429

25-
export default function configureStore(initialState = undefined) {
30+
export default function configureStore(initialState?: DeepPartial<RootState>) {
2631
const store = createStore(rootReducer, initialState, enhancer);
2732
sagaMiddleware.run(rootSaga);
2833
return store;

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

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

7-
// TODO: do we need this?
8-
interface RootState<StatePart> {
9-
[statePart: string]: StatePart;
7+
interface RootState {
8+
[statePart: string]: any;
109
}
1110

1211
// Provide action keys for auto-complete but allow custom types
@@ -41,13 +40,16 @@ declare module 'reducktion' {
4140
failure: Reducer<State>;
4241
}
4342

43+
type NoopReducer = (state: any) => any;
44+
4445
interface Fetchable {
4546
value: <T>(val: T) => FetchableValue<T>;
4647
action: <State, K extends keyof State>(
4748
// Only allow state fields for fetchable values
4849
stateField: FetchableValue extends State[K] ? K : never,
4950
customReducers?: Partial<FetchableReducers<State>>
5051
) => FetchableReducers<State>;
52+
noop: () => NoopReducer;
5153
}
5254

5355
// TODO:
@@ -65,7 +67,7 @@ declare module 'reducktion' {
6567
? FetchableReducers<State>
6668
: Actions[K] extends Function
6769
? Reducer<State, ArgumentType<Actions[K]>>
68-
: never
70+
: never;
6971
};
7072
reactions?: (
7173
{ initialState, deps }: { initialState: State; deps: Deps }
@@ -92,7 +94,7 @@ declare module 'reducktion' {
9294
selectors: Selectors & {
9395
get: <K extends keyof State>(
9496
stateField: K
95-
) => (state: RootState<State>, ...args: any[]) => Pick<State, K>[K];
97+
) => (state: RootState, ...args: any[]) => Pick<State, K>[K];
9698
};
9799
getSagas: () => [];
98100
getReducer: () => Reducer<any>;

reducktion.test.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77

88
// TODO: fix tests related to auto-generated selectors!
99

10-
describe('fetchable/fetchable.action', () => {
10+
describe('fetchable', () => {
1111
it('should create fetchable value', () => {
1212
const f = fetchable.value([]);
1313
expect(f.data).toEqual([]);
@@ -25,6 +25,12 @@ describe('fetchable/fetchable.action', () => {
2525
expect(fa2.args[0]).toEqual('orders');
2626
expect(fa2.args[1].loading).toBeInstanceOf(Function);
2727
});
28+
29+
it('should create fetchable no-op reducer', () => {
30+
const reducer = fetchable.noop();
31+
const state = { test: 1, test2: 2 };
32+
expect(reducer(state)).toEqual(state);
33+
});
2834
});
2935

3036
describe('createModel', () => {

src/reducktion.d.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ type ArgumentType<F extends Function> = F extends (arg: infer A) => any
33
? A
44
: never;
55

6-
// TODO: do we need this?
7-
interface RootState<StatePart> {
8-
[statePart: string]: StatePart;
6+
interface RootState {
7+
[statePart: string]: any;
98
}
109

1110
// Provide action keys for auto-complete but allow custom types
@@ -40,13 +39,16 @@ interface FetchableReducers<State> {
4039
failure: Reducer<State>;
4140
}
4241

42+
type NoopReducer = (state: any) => any;
43+
4344
interface Fetchable {
4445
value: <T>(val: T) => FetchableValue<T>;
4546
action: <State, K extends keyof State>(
4647
// Only allow state fields for fetchable values
4748
stateField: FetchableValue extends State[K] ? K : never,
4849
customReducers?: Partial<FetchableReducers<State>>
4950
) => FetchableReducers<State>;
51+
noop: () => NoopReducer;
5052
}
5153

5254
// TODO:
@@ -64,7 +66,7 @@ interface ModelDefinition<State, Actions, Selectors, Deps> {
6466
? FetchableReducers<State>
6567
: Actions[K] extends Function
6668
? Reducer<State, ArgumentType<Actions[K]>>
67-
: never
69+
: never;
6870
};
6971
reactions?: (
7072
{ initialState, deps }: { initialState: State; deps: Deps }
@@ -91,7 +93,7 @@ interface Model<State, Actions, Selectors> {
9193
selectors: Selectors & {
9294
get: <K extends keyof State>(
9395
stateField: K
94-
) => (state: RootState<State>, ...args: any[]) => Pick<State, K>[K];
96+
) => (state: RootState, ...args: any[]) => Pick<State, K>[K];
9597
};
9698
getSagas: () => [];
9799
getReducer: () => Reducer<any>;

src/reducktion.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,7 @@ export const fetchable = {
193193
[FETCHABLE_ACTION_IDENTIFIER]: true,
194194
args,
195195
}),
196+
197+
// No-op reducer for cases when an action does not update state
198+
noop: () => state => state,
196199
};

0 commit comments

Comments
 (0)