Skip to content

Commit

Permalink
Create reduceReducers util
Browse files Browse the repository at this point in the history
  • Loading branch information
EskiMojo14 committed Sep 26, 2024
1 parent 05ba9d1 commit 31f9767
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { kindOf } from './utils/kindOf'
*
* @internal
*/
type NoInfer<T> = [T][T extends any ? 0 : never]
export type NoInfer<T> = [T][T extends any ? 0 : never]

/**
* @deprecated
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// functions
import { createStore, legacy_createStore } from './createStore'
import combineReducers from './combineReducers'
import reduceReducers from './reduceReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
Expand Down Expand Up @@ -41,6 +42,7 @@ export {
createStore,
legacy_createStore,
combineReducers,
reduceReducers,
bindActionCreators,
applyMiddleware,
compose,
Expand Down
59 changes: 59 additions & 0 deletions src/reduceReducers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { NoInfer } from './createStore'
import type { Action } from './types/actions'
import type { Reducer } from './types/reducers'

/**
* Composes multiple reducers into one.
*
* @param initialState The initial state, which can be a different preloaded state.
* @param reducer The first reducer. Can accept a different preloaded state.
* @param reducers The rest of the reducers.
* @returns A reducer function that invokes every reducer passed in order, and returns the result of the last reducer.
*/
export default function reduceReducers<
S,
A extends Action,
Actions extends Action[],
P
>(
initialState: NoInfer<P | S> | undefined,
reducer: Reducer<S, A, P>,
...reducers: {
[K in keyof Actions]: Reducer<S, Actions[K]>
}
): Reducer<S, A | Actions[number], P>
/**
* Composes multiple reducers into one.
*
* @param reducer The first reducer. Can accept a different preloaded state.
* @param reducers The rest of the reducers.
* @returns A reducer function that invokes every reducer passed in order, and returns the result of the last reducer.
*/
export default function reduceReducers<
S,
A extends Action,
Actions extends Action[],
P
>(
reducer: Reducer<S, A, P>,
...reducers: {
[K in keyof Actions]: Reducer<S, Actions[K]>
}
): Reducer<S, A | Actions[number], P>
export default function reduceReducers<S, A extends Action, P>(
...args: [P | S | undefined | Reducer<S, A, P>, ...Array<Reducer<S, A>>]
): Reducer<S, A, P> {
const initialState =
typeof args[0] === 'function'
? undefined
: (args.shift() as P | S | undefined)
const [firstReducer, ...restReducers] = args as [
Reducer<S, A, P>,
...Reducer<S, A>[]
]
return (state = initialState, action) =>
restReducers.reduce(
(state, reducer) => reducer(state, action),
firstReducer(state, action)
)
}
34 changes: 34 additions & 0 deletions test/reduceReducers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import reduceReducers from "@internal/reduceReducers";

describe('Utils', () => {
describe('reduceReducers', () => {
const incrementReducer = (state = 0, action: { type: "increment" }) =>
action.type === 'increment' ? state + 1 : state
const decrementReducer = (state = 0, action: { type: "decrement" }) =>
action.type === 'decrement' ? state - 1 : state

it("runs multiple reducers in sequence and returns the result of the last one", () => {
const combined = reduceReducers(incrementReducer, decrementReducer)
expect(combined(0, { type: 'increment' })).toBe(1)
expect(combined(1, { type: 'decrement' })).toBe(0)
})
it("accepts an initial state argument", () => {
const combined = reduceReducers(2, incrementReducer, decrementReducer)
expect(combined(undefined, { type: "increment" })).toBe(3)
})
it("can accept the preloaded state of the first reducer", () => {
const parserReducer = (state: number | string = 0) =>
typeof state === 'string' ? parseInt(state, 10) : state

const combined = reduceReducers(parserReducer, incrementReducer)
expect(combined("1", { type: "increment"})).toBe(2)

const combined2 = reduceReducers("1", parserReducer, incrementReducer)
expect(combined2(undefined, { type: "increment"})).toBe(2)
})
it("accepts undefined as initial state", () => {
const combined = reduceReducers(undefined, incrementReducer)
expect(combined(undefined, { type: "increment" })).toBe(1)
})
});
})

0 comments on commit 31f9767

Please sign in to comment.