Skip to content

Commit 31f9767

Browse files
committed
Create reduceReducers util
1 parent 05ba9d1 commit 31f9767

File tree

4 files changed

+96
-1
lines changed

4 files changed

+96
-1
lines changed

src/createStore.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { kindOf } from './utils/kindOf'
2121
*
2222
* @internal
2323
*/
24-
type NoInfer<T> = [T][T extends any ? 0 : never]
24+
export type NoInfer<T> = [T][T extends any ? 0 : never]
2525

2626
/**
2727
* @deprecated

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// functions
22
import { createStore, legacy_createStore } from './createStore'
33
import combineReducers from './combineReducers'
4+
import reduceReducers from './reduceReducers'
45
import bindActionCreators from './bindActionCreators'
56
import applyMiddleware from './applyMiddleware'
67
import compose from './compose'
@@ -41,6 +42,7 @@ export {
4142
createStore,
4243
legacy_createStore,
4344
combineReducers,
45+
reduceReducers,
4446
bindActionCreators,
4547
applyMiddleware,
4648
compose,

src/reduceReducers.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { NoInfer } from './createStore'
2+
import type { Action } from './types/actions'
3+
import type { Reducer } from './types/reducers'
4+
5+
/**
6+
* Composes multiple reducers into one.
7+
*
8+
* @param initialState The initial state, which can be a different preloaded state.
9+
* @param reducer The first reducer. Can accept a different preloaded state.
10+
* @param reducers The rest of the reducers.
11+
* @returns A reducer function that invokes every reducer passed in order, and returns the result of the last reducer.
12+
*/
13+
export default function reduceReducers<
14+
S,
15+
A extends Action,
16+
Actions extends Action[],
17+
P
18+
>(
19+
initialState: NoInfer<P | S> | undefined,
20+
reducer: Reducer<S, A, P>,
21+
...reducers: {
22+
[K in keyof Actions]: Reducer<S, Actions[K]>
23+
}
24+
): Reducer<S, A | Actions[number], P>
25+
/**
26+
* Composes multiple reducers into one.
27+
*
28+
* @param reducer The first reducer. Can accept a different preloaded state.
29+
* @param reducers The rest of the reducers.
30+
* @returns A reducer function that invokes every reducer passed in order, and returns the result of the last reducer.
31+
*/
32+
export default function reduceReducers<
33+
S,
34+
A extends Action,
35+
Actions extends Action[],
36+
P
37+
>(
38+
reducer: Reducer<S, A, P>,
39+
...reducers: {
40+
[K in keyof Actions]: Reducer<S, Actions[K]>
41+
}
42+
): Reducer<S, A | Actions[number], P>
43+
export default function reduceReducers<S, A extends Action, P>(
44+
...args: [P | S | undefined | Reducer<S, A, P>, ...Array<Reducer<S, A>>]
45+
): Reducer<S, A, P> {
46+
const initialState =
47+
typeof args[0] === 'function'
48+
? undefined
49+
: (args.shift() as P | S | undefined)
50+
const [firstReducer, ...restReducers] = args as [
51+
Reducer<S, A, P>,
52+
...Reducer<S, A>[]
53+
]
54+
return (state = initialState, action) =>
55+
restReducers.reduce(
56+
(state, reducer) => reducer(state, action),
57+
firstReducer(state, action)
58+
)
59+
}

test/reduceReducers.spec.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import reduceReducers from "@internal/reduceReducers";
2+
3+
describe('Utils', () => {
4+
describe('reduceReducers', () => {
5+
const incrementReducer = (state = 0, action: { type: "increment" }) =>
6+
action.type === 'increment' ? state + 1 : state
7+
const decrementReducer = (state = 0, action: { type: "decrement" }) =>
8+
action.type === 'decrement' ? state - 1 : state
9+
10+
it("runs multiple reducers in sequence and returns the result of the last one", () => {
11+
const combined = reduceReducers(incrementReducer, decrementReducer)
12+
expect(combined(0, { type: 'increment' })).toBe(1)
13+
expect(combined(1, { type: 'decrement' })).toBe(0)
14+
})
15+
it("accepts an initial state argument", () => {
16+
const combined = reduceReducers(2, incrementReducer, decrementReducer)
17+
expect(combined(undefined, { type: "increment" })).toBe(3)
18+
})
19+
it("can accept the preloaded state of the first reducer", () => {
20+
const parserReducer = (state: number | string = 0) =>
21+
typeof state === 'string' ? parseInt(state, 10) : state
22+
23+
const combined = reduceReducers(parserReducer, incrementReducer)
24+
expect(combined("1", { type: "increment"})).toBe(2)
25+
26+
const combined2 = reduceReducers("1", parserReducer, incrementReducer)
27+
expect(combined2(undefined, { type: "increment"})).toBe(2)
28+
})
29+
it("accepts undefined as initial state", () => {
30+
const combined = reduceReducers(undefined, incrementReducer)
31+
expect(combined(undefined, { type: "increment" })).toBe(1)
32+
})
33+
});
34+
})

0 commit comments

Comments
 (0)