Skip to content

Commit

Permalink
feat: Reducer constructor returns a reducer function
Browse files Browse the repository at this point in the history
This commit makes the reducer constructor (`reducer`) return a function
that can be called like a plain reducer function. This allows for easier
composition of reducers.

BREAKING CHANGE: `ReducerEntry` has been renamed to `RegisteredReducer`,
and changed shape from an array to a function with a `trigger` property.
  • Loading branch information
tlaundal committed Feb 19, 2020
1 parent 827e386 commit 9efb05a
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 38 deletions.
9 changes: 7 additions & 2 deletions examples/reducers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { actionCreator, reducer, combineReducers, ReducerEntry } from 'rxbeach';
import {
actionCreator,
reducer,
combineReducers,
RegisteredReducer,
} from 'rxbeach';
import test from 'ava';
import { marbles } from 'rxjs-marbles/ava';

Expand All @@ -8,7 +13,7 @@ const incrementMany = actionCreator<number>('[increment] many');

// Our reducers
type CounterState = number;
type CounterReducer = ReducerEntry<CounterState>;
type CounterReducer = RegisteredReducer<CounterState>;

// Style 1 - Define the state type on state argument
const handleOne = reducer(incrementOne, (prev: CounterState) => prev + 1);
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export {
reducer,
combineReducers,
Reducer,
ReducerEntry,
RegisteredReducer,
} from 'rxbeach/reducer';

export {
Expand Down
11 changes: 6 additions & 5 deletions src/reducer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { reducer, combineReducers, actionCreator } from 'rxbeach';
import test from 'ava';
import { marbles } from 'rxjs-marbles/ava';
import sinon from 'sinon';

const throwErrorFn = (): number => {
throw 'error';
Expand All @@ -9,10 +10,8 @@ const incrementOne = actionCreator('[increment] one');
const decrement = actionCreator('[increment] decrement');
const incrementMany = actionCreator<number>('[increment] many');

const handleOne = reducer(
incrementOne,
(accumulator: number) => accumulator + 1
);
const incrementOneHandler = sinon.spy((accumulator: number) => accumulator + 1);
const handleOne = reducer(incrementOne, incrementOneHandler);
const handleMany = reducer(
incrementMany,
(accumulator: number, increment) => accumulator + increment
Expand All @@ -32,7 +31,9 @@ const outputs = {
};

test('reducer should store reducer function', t => {
t.deepEqual(handleDecrement, [[decrement], throwErrorFn]);
incrementOneHandler.resetHistory();
handleOne(1);
t.assert(incrementOneHandler.called);
});

test(
Expand Down
67 changes: 37 additions & 30 deletions src/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ export type Reducer<State, Payload = VoidPayload> = (
payload: Payload
) => State;

export type ReducerEntry<State, Payload = any> = [
UnknownActionCreator[],
Reducer<State, Payload>
];
export type RegisteredReducer<State, Payload = any> = Reducer<
State,
Payload
> & {
trigger: {
actions: UnknownActionCreator[];
};
};

type ReducerCreator = {
/**
Expand All @@ -30,13 +34,13 @@ type ReducerCreator = {
* @template `Payload` - The payload of the action, fed to the reducer together
* with the state. Should be automatically extracted from
* the `actionCreator` parameter
* @returns A reducer entry; A tuple array of actions and reducer, for passing
* into `combineReducers`
* @returns A registered reducer that can be passed into `combineReducers`, or
* called directly as if it was the `reducer` parameter itself.
*/
<State, Payload>(
actionCreator: UnknownActionCreatorWithPayload<Payload>,
reducer: Reducer<State, Payload>
): ReducerEntry<State, Payload>;
): RegisteredReducer<State, Payload>;

/**
* Define a reducer for an action without payload
Expand All @@ -46,13 +50,13 @@ type ReducerCreator = {
* extract payload type from
* @param reducer The reducer function
* @template `State` - The state the reducer reduces to
* @returns A reducer entry; A tuple array of actions and reducer, for passing
* into `combineReducers`
* @returns A registered reducer that can be passed into `combineReducers`, or
* called directly as if it was the `reducer` parameter itself.
*/
<State>(
actionCreator: UnknownActionCreator,
reducer: Reducer<State, VoidPayload>
): ReducerEntry<State, VoidPayload>;
): RegisteredReducer<State, VoidPayload>;

/**
* Define a reducer for multiple actions with overlapping payload
Expand All @@ -65,13 +69,13 @@ type ReducerCreator = {
* @template `Payload` - The payload of the action, fed to the reducer together
* with the state. Should be automatically extracted from
* the `actionCreator` parameter
* @returns A reducer entry; A tuple array of actions and reducer, for passing
* into `combineReducers`
* @returns A registered reducer that can be passed into `combineReducers`, or
* called directly as if it was the `reducer` parameter itself.
*/
<State, Payload>(
actionCreator: UnknownActionCreatorWithPayload<Payload>[],
reducer: Reducer<State, Payload>
): ReducerEntry<State, Payload>;
): RegisteredReducer<State, Payload>;

/**
* Define a reducer for multiple actions without overlapping payload
Expand All @@ -81,13 +85,13 @@ type ReducerCreator = {
* extract payload type from
* @param reducer The reducer function
* @template `State` - The state the reducer reduces to
* @returns A reducer entry; A tuple array of actions and reducer, for passing
* into `combineReducers`
* @returns A registered reducer that can be passed into `combineReducers`, or
* called directly as if it was the `reducer` parameter itself.
*/
<State>(
actionCreator: UnknownActionCreatorWithPayload<{}>[],
reducer: Reducer<State, {}>
): ReducerEntry<State, {}>;
): RegisteredReducer<State, {}>;

/**
* Define a reducer for multiple actions without payloads
Expand All @@ -97,22 +101,25 @@ type ReducerCreator = {
* extract payload type from
* @param reducer The reducer function
* @template `State` - The state the reducer reduces to
* @returns A reducer entry; A tuple array of actions and reducer, for passing
* into `combineReducers`
* @returns A registered reducer that can be passed into `combineReducers`, or
* called directly as if it was the `reducer` parameter itself.
*/
<State>(
actionCreator: UnknownActionCreator[],
reducer: Reducer<State, VoidPayload>
): ReducerEntry<State, VoidPayload>;
): RegisteredReducer<State, VoidPayload>;
};

export const reducer: ReducerCreator = <State>(
actionCreator: UnknownActionCreator | UnknownActionCreator[],
reducerFn: Reducer<State, any>
): ReducerEntry<State, unknown> => [
Array.isArray(actionCreator) ? actionCreator : [actionCreator],
reducerFn,
];
): RegisteredReducer<State, unknown> => {
const wrapper = (state: State, payload: any) => reducerFn(state, payload);
wrapper.trigger = {
actions: Array.isArray(actionCreator) ? actionCreator : [actionCreator],
};
return wrapper;
};

/**
* Combine reducer entries into a stream operator
Expand All @@ -128,23 +135,23 @@ export const reducer: ReducerCreator = <State>(
*/
export const combineReducers = <State>(
seed: State,
reducers: ReducerEntry<State, any>[]
reducers: RegisteredReducer<State, any>[]
): OperatorFunction<UnknownAction, State> => {
const reducersByActionType = new Map(
reducers.flatMap(([actions, reducerEntry]) =>
actions.map(action => [action.type, reducerEntry])
reducers.flatMap(reducerFn =>
reducerFn.trigger.actions.map(action => [action.type, reducerFn])
)
);
return pipe(
ofType(...reducers.flatMap(([action]) => action)),
ofType(...reducers.flatMap(reducerFn => reducerFn.trigger.actions)),
scan((state, { type, payload }: UnknownAction) => {
const reducerEntry = reducersByActionType.get(type);
if (reducerEntry === undefined) {
const RegisteredReducer = reducersByActionType.get(type);
if (RegisteredReducer === undefined) {
// This shouldn't be possible
return state;
}

return reducerEntry(state, payload);
return RegisteredReducer(state, payload);
}, seed)
);
};

0 comments on commit 9efb05a

Please sign in to comment.