DEV Community

Ken Aguilar
Ken Aguilar

Posted on • Edited on

Typescript - Using Option Data Type in Redux Reducer

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 getOrElse in the bottom will return state if ever there is None in the chain.

Here's the type signature of getOrElse

getOrElse

(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).

Top comments (0)