One of my concerns with reducers in redux is it can grow to an infinite size. Which I noticed is currently happening to our reducers.
Since I can’t show our code here’s a contrived example instead.
const listLens = Lens.fromProp<State, "list">("list");
const newUserLens = Lens.fromProp<State, "newUser">("newUser");
const initUserLens = Lens.fromProp<NewUserState, "initial">("initial");
const confrmUserLens = Lens.fromProp<NewUserState, "confirmed">("confirmed");
const isBlankUser = (u: User) ⇒ u.firstName ≠ undefined;
const setList = (state: State, action: RAction<Response>) ⇒
listLens.modify(l ⇒
filter(isBlankUser)(concat(action.payload.items, l))
)(state);
const setInitial = (state: State, action: RAction<User>) ⇒
newUserLens
.compose(initUserLens)
.modify(x ⇒ merge(x, action.payload))(state);
const setConfirmed = (state: State) ⇒
newUserLens
.compose(confrmUserLens)
.modify(x ⇒
merge(x, newUserLens.compose(initUserLens).get(state))
)(state);
const setEdited = (state: State, action: RAction<Res<User>>) ⇒
state;
const resetNewUser = (state: State) ⇒
newUserLens.set(newUserLens.get(initialState))(state);
const resetState = (state: State) ⇒ initialState;
export const userList = (state: State = initialState, action: Action) ⇒ {
switch (action.type) {
case SET_USER_LIST:
return setList(state, action);
case SET_INITIAL_NEW_USER:
return setInitial(state, action);
case SET_CONFIRMED_NEW_USER:
return setConfirmed(state, action);
case SET_EDITED_USER:
return setEditedUser(state, action);
CASE RESET_NEW_USER_STATE:
return resetNewUser(state, action);
case RESET_STATE:
return resetState(state);
// imagine more case statements here. Maybe 50 more...
default:
return state;
}
};As you can see it can grow to have more lines!
Luckily, I found this article by Vinicius Gomes. It talks how you can
reduce the boilerplate in your reducer by using the Maybe type. It will get rid of
the ever growing size of cases in a typical reducer that is written with a switch
statement.
The code snippet above can turn into this.
import { fromNullable } from "fp-ts/lib/Option";
import { filter, concat, merge } from "ramda";
import { Lens } from "monocle-ts";
import { State, Action, RAction, User, Res, NewUserState } from "./types";
import { initialUser, initialNewUser } from "./initial-values";
const initialState: State = {
list: [initialUser],
newUser: {
initial: initialNewUser,
confirmed: initialNewUser
},
selectedUser: initialUser
};
type Response = Res<ReadonlyArray<User>>;
interface Handlers {
[type: string]: (s: State, a: Action) ⇒ State;
}
const listLens = Lens.fromProp<State, "list">("list");
const newUserLens = Lens.fromProp<State, "newUser">("newUser");
const initUserLens = Lens.fromProp<NewUserState, "initial">("initial");
const confrmUserLens = Lens.fromProp<NewUserState, "confirmed">("confirmed");
const isBlankUser = (u: User) ⇒ u.firstName ≠ undefined;
const SET_USER_LIST = (state: State, action: RAction<Response>) ⇒
listLens.modify(l ⇒
filter(isBlankUser)(concat(action.payload.items, l))
)(state);
const SET_INITIAL_NEW_USER = (state: State, action: RAction<User>) ⇒
newUserLens
.compose(initUserLens)
.modify(x ⇒ merge(x, action.payload))(state);
const SET_CONFIRMED_NEW_USER = (state: State) ⇒
newUserLens
.compose(confrmUserLens)
.modify(x ⇒
merge(x, newUserLens.compose(initUserLens).get(state))
)(state);
const SET_EDITED_USER = (state: State, action: RAction<Res<User>>) ⇒
state;
const RESET_NEW_USER = (state: State) ⇒
newUserLens.modify(() ⇒ newUserLens.get(initialState))(state);
const RESET_STATE = (state: State) ⇒ initialState;
const actionHandlers: Handlers = {
SET_USER_LIST,
SET_INITIAL_NEW_USER,
SET_CONFIRMED_NEW_USER,
SET_EDITED_USER,
RESET_NEW_USER,
RESET_STATE
};
export const userList = (state: State = initialState, action: Action) ⇒
fromNullable(actionHandlers[action.type])
.map(f ⇒ f(state, action))
.getOrElseValue(state);Instead of using the Maybe type I used Option type from fp-ts.
Option and Maybe types are synonymous.
According to fp-ts
fromNullable
<A>(a: A | null | undefined): Option<A>In this context, if actionHandlers[action.type] comes up undefined it will return
the data constructor None, and getOrElseValue in the bottom will return state
if ever there is None in the chain.
Here’s the type signature of getOrElseValue
getOrElseValue
(a: A): AWhen an incoming type matches one of my functions in actionHandlers then map
will apply that function to state.
Finally, I change the names on my reducer functions, and delete the long line of imported constants.
Conclusion
I’ve changed the reducer body to have less moving parts. Instead of having many case statements
it now only has those 3 chained function calls. I also got rid of importing the action-creator
constants(i.e SET_USER_LIST).