Skip to content

Commit 149d2c3

Browse files
authored
Merge pull request #5 from jaystack/refact-middlewares
Refact middlewares
2 parents bde7cea + f4437fc commit 149d2c3

5 files changed

Lines changed: 79 additions & 66 deletions

File tree

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,16 @@ const setX = x => reduceFoo(state => ({ ...state, x }));
8484

8585
## Middlewares
8686

87-
A repatch middleware takes the store instance and the previous reducer and returns a new reducer:
87+
A repatch middleware takes the `store` instance, a `next` function and the previous `reducer`. The middleware can provide a new reducer via the `next` function.
8888

8989
```javascript
90-
(Store, Reducer): Reducer
90+
Middleware: Store -> (Reducer -> Reducer) -> Reducer -> any
91+
```
92+
93+
where
94+
95+
```javascript
96+
Next: Reducer -> Reducer
9197
```
9298

9399
Use the `addMiddleware` method to chaining middlewares:
@@ -98,6 +104,21 @@ const store = new Store(initialState)
98104
.addMiddleware(mw2, mw3);
99105
```
100106

107+
## Middleware example
108+
109+
This simple logger middleware logs the current- and the next state:
110+
111+
```javascript
112+
const logger = store => next => reducer => {
113+
const state = store.getState()
114+
const nextState = reducer(state)
115+
console.log(state, nextState)
116+
return next(_ => nextState)
117+
}
118+
119+
const store = new Store(initialState).addMiddleware(logger)
120+
```
121+
101122
## Async actions
102123

103124
The `thunk` middleware is useful for handling async actions similar to [redux-thunk](https://www.npmjs.com/package/redux-thunk).

docs/store.md

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ Dispatches a reducer.
4646

4747
#### Arguments
4848

49-
1) **reducer** (*ReducerFunction: State -> State*): That reducer will reduce the state of the store. This takes the current state and returns the next state. The reducer will be run synchronously after applying the middlewares - if they are given.
49+
1) **reducer** (*ReducerFunction: State -> State*): That reducer will reduce the state of the store. This takes the current state and returns the next state.
5050

5151
#### Returns
5252

53-
(*ReducerFunction*): The final reducer that is made by applying the middlewares - if they are given. If the store does not have middlewares, the `dispatch` method returns the same reducer that was taken as argument.
53+
(*ReducerFunction*): The final reducer that is made by applying the middlewares - if they are given. If the store does enhanced with middlewares, the `dispatch` method returns the same reducer that was taken as argument.
5454

5555
#### Example
5656

@@ -66,8 +66,7 @@ console.log(result === increment) // true
6666

6767
#### Notes
6868

69-
1) Middlewares can modify the given reducer, so that is not guaranteed that the `dispatch` returns the original reducer that was taken as argument.
70-
2) The `dispatch` only runs the reducer, when the final reducer (after applying middlewares) is still a function. If the final reducer is a function, then the dispatch modifies the state by its returned value. This behaviour is strongly used by the [thunk](thunk.md) middleware, that returns the returned value of the delegate.
69+
Middlewares can modify the given reducer, so that is not guaranteed that the `dispatch` returns the original reducer that was taken as argument.
7170

7271
### `subscribe(listener)`
7372

@@ -99,33 +98,32 @@ store.dispatch(increment) // listener won't be fired
9998

10099
### `addMiddleware(...middlewares)`
101100

102-
Adds middleware(s) to the store.
101+
Enhances the store with the given middleware(s).
103102

104-
Middlewares will be run at dispatching before the store applies the new state of the reducer. The added middlewares are composed by order of addition.
103+
Middlewares will be run at dispatching before the store applies the new state of the reducer. The added middlewares are composed by order of addition, so the last added middleware will run first.
105104

106105
#### Arguments
107106

108-
1) **...middlewares** (*MiddlewareFunction: (Store, Reducer) -> Reducer*): Middlewares as variadic arguments. Middleware functions take the `Store` instance and the dispatched reducer and return a new reducer.
107+
1) **...middlewares** (*MiddlewareFunction: Store -> Next -> Reducer -> any*): Middlewares as variadic arguments. Middleware functions take the `store` instance, a `next` function and the previous `reducer`. The middleware can provide a new reducer via the `next` function.
109108

110109
#### Returns
111110

112-
(*Store: this*): Returns the same `Store` instance for chaining.
111+
(*Store: this*): Returns the enhanced `Store` instance for chaining.
113112

114113
#### Example
115114

116115
```javascript
117-
const store = new Store({ counter: 0 })
118-
119-
const logger = (store, reducer) => {
120-
const nextState = reducer(store.getState())
121-
console.log(nextState)
122-
return _ => nextState
116+
const logger = store => next => reducer => {
117+
const state = store.getState()
118+
const nextState = reducer(state)
119+
console.log(state, nextState)
120+
return next(_ => nextState)
123121
}
124122

125-
store.addMiddleware(logger)
123+
const store = new Store({ counter: 0 }).addMiddleware(logger)
126124

127125
store.dispatch(state => ({ counter: state.counter + 1 }))
128-
// logger logs { counter: 1 }
126+
// logger logs { counter: 0 } { counter: 1 }
129127
```
130128

131129
## Static members

src/store/index.ts

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export default class Store<State, R = Reducer<State>> implements IStore<State, R
1010

1111
private state: State;
1212
private listeners: Function[] = [];
13-
private middlewares: Function[] = [];
1413

1514
constructor(initialState: State) {
1615
this.state = initialState;
@@ -19,43 +18,28 @@ export default class Store<State, R = Reducer<State>> implements IStore<State, R
1918
getState: GetState<State> = () => this.state;
2019

2120
dispatch: Dispatch<R> = reducer => {
22-
assertReducer(reducer);
23-
const finalReducer = this.applyMiddlewares(reducer);
24-
if (typeof finalReducer === 'function') {
25-
this.state = finalReducer(this.state);
26-
this.listeners.forEach(listener => listener());
27-
}
28-
return <R>finalReducer;
21+
if (typeof reducer !== 'function')
22+
throw new Error('Reducer is not a function: dispatch takes only reducers as functions.');
23+
this.state = reducer(this.state);
24+
this.listeners.forEach(listener => listener());
25+
return <R>reducer;
2926
};
3027

3128
subscribe = (listener: Listener): Unsubscribe => {
32-
assertListener(listener);
29+
if (typeof listener !== 'function')
30+
throw new Error('Listener is not a function: subscribe takes only listeners as functions.');
3331
this.listeners = [ ...this.listeners, listener ];
3432
return () => (this.listeners = this.listeners.filter(lis => lis !== listener));
3533
};
3634

37-
addMiddleware = <R2>(...middlewares: Middleware<State>[]): Store<State, R | R2> => {
38-
assertMiddlewares(middlewares);
39-
this.middlewares = [ ...this.middlewares, ...middlewares ];
35+
addMiddleware = <R2>(...middlewares: Middleware<State, R, R2>[]): Store<State, R | R2> => {
36+
if (middlewares.some(middleware => typeof middleware !== 'function'))
37+
throw new Error('Middleware is not a function: addMiddleware takes only middlewares as functions.');
38+
middlewares.forEach(middleware => {
39+
const prevDispatch = this.dispatch;
40+
const dispatch = reducer => middleware(this)(prevDispatch)(reducer);
41+
this.dispatch = dispatch;
42+
});
4043
return this;
4144
};
42-
43-
private applyMiddlewares = (reducer: R): R =>
44-
<R>this.middlewares.reduce((prevReducer, middleware) => middleware(this, prevReducer), reducer);
45-
}
46-
47-
function assertReducer(reducer) {
48-
if (typeof reducer !== 'function')
49-
throw new Error('Reducer is not a function: dispatch takes only reducers as functions.');
50-
}
51-
52-
function assertListener(listener) {
53-
if (typeof listener !== 'function')
54-
throw new Error('Listener is not a function: subscribe takes only listeners as functions.');
55-
}
56-
57-
function assertMiddlewares(middlewares) {
58-
if (middlewares.some(middleware => typeof middleware !== 'function')) {
59-
throw new Error('Middleware is not a function: addMiddleware takes only middlewares as functions.');
60-
}
6145
}

src/store/middlewares/thunk.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
11
import { Middleware, GetState, Dispatch, Reducer, Store } from '../types';
22

3-
export interface ThunkMiddleware<State, ExtraArgument> extends Middleware<State> {
4-
withExtraArgument: (extraArgument: ExtraArgument) => ThunkMiddleware<State, ExtraArgument>;
3+
export interface ThunkMiddleware<State, ExtraArgument>
4+
extends Middleware<State, Reducer<State>, Thunk<State, ExtraArgument, any>> {
5+
withExtraArgument: <EA>(extraArgument: EA) => ThunkMiddleware<State, EA>;
56
}
67

7-
export interface Delegate<State, ExtraArgument, T> {
8-
(dispatch: ThunkDispatch<State, ExtraArgument>, getState: GetState<State>, extraArgument: ExtraArgument): T;
8+
export interface Delegate<State, ExtraArgument, Return> {
9+
(dispatch: ThunkDispatch<State, ExtraArgument>, getState: GetState<State>, extraArgument: ExtraArgument): Return;
910
}
1011

1112
export interface ThunkDispatch<State, ExtraArgument> extends Dispatch<Reducer<State>> {
12-
<T>(reducer: Thunk<State, ExtraArgument, T>): T;
13+
<Return>(reducer: Thunk<State, ExtraArgument, Return>): Return;
1314
}
1415

15-
export interface Thunk<State, ExtraArgument, T> {
16-
(state: State): Delegate<State, ExtraArgument, T>;
16+
export interface Thunk<State, ExtraArgument, Return> {
17+
(state: State): Delegate<State, ExtraArgument, Return>;
1718
}
1819

19-
function thunkFactory<T>(extraArgument?: T): ThunkMiddleware<any, T> {
20-
const thunk = ((store, reducer) => {
20+
const thunkFactory = (extraArgument?) => {
21+
const thunk = store => next => reducer => {
22+
if (typeof reducer !== 'function') throw new Error('Thunk requires reducers as functions');
2123
const state = store.getState();
2224
const result = reducer(state);
23-
return typeof result === 'function' ? result(store.dispatch, store.getState, extraArgument) : reducer;
24-
}) as ThunkMiddleware<any, T>;
25-
thunk.withExtraArgument = thunkFactory;
25+
if (typeof result === 'function') return result(store.dispatch, store.getState, extraArgument);
26+
else {
27+
next(_ => result);
28+
return reducer;
29+
}
30+
};
31+
thunk['withExtraArgument'] = thunkFactory;
2632
return thunk;
27-
}
33+
};
2834

29-
export default thunkFactory();
35+
export default thunkFactory() as ThunkMiddleware<any, any>;

src/store/types.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@ export interface Unsubscribe {
1818
(): void;
1919
}
2020

21-
export interface Middleware<State> {
22-
(store: Store<State>, reducer: Reducer<State>): Reducer<State>;
21+
export interface Middleware<State, R1, R2> {
22+
(store: Store<State, R1>): {
23+
(next: Dispatch<R1>): {
24+
(reducer: R2): any;
25+
}
26+
};
2327
}
2428

2529
export interface Store<State, R = Reducer<State>> {
2630
getState: GetState<State>;
2731
dispatch: Dispatch<R>;
2832
subscribe(listener: Listener): Unsubscribe;
29-
addMiddleware<R2>(...middlewares: Middleware<State>[]): Store<State, R | R2>;
33+
addMiddleware<R2>(...middlewares: Middleware<State, R, R2>[]): Store<State, R | R2>;
3034
}

0 commit comments

Comments
 (0)