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>) ⇒
.modify(l ⇒
listLensfilter(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) ⇒
.set(newUserLens.get(initialState))(state);
newUserLens
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);
RESET_NEW_USER_STATE:
CASE 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 case
s 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
;
}
= Res<ReadonlyArray<User>>;
type Response
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>) ⇒
.modify(l ⇒
listLensfilter(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) ⇒
.modify(() ⇒ newUserLens.get(initialState))(state);
newUserLens
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 (a
When 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
).