/ react

Replace Conditional With Map Refactoring

This is one of my favorite refactorings. It helps to group logic, making code easier to read and extend.

I'm writing this blog post to use as a bookmark.

Very often, I see code which looks like the following:

function Card({ card }) {
  if (card.type === 'news') {
    return <NewsCard card={card} />;
  }

  if (card.type === 'post') {
    return <PostCard card={card} />;
  }

  if (card.type === 'user') {
    return <UserCard card={card} />;
  }

  return <DefaultCard card={card} />;
}

The other variant, I see the following is in its switch form:

function Card({ card }) {
  switch(card.type) {
    case 'news':
      return <NewsCard card={card} />;
    case 'post':
      return <PostCard card={card} />;
    case 'user':
      return <UserCard card={card} />;
    default:
      return <DefaultCard card={card} />;
  }
}

My biggest gripe with this code is that it is noisy. There is so much syntax here that it makes it easy to hide extra details. When there are multiple cases, when you get to the end, you have already forgotten what the first option was.

The way to simplify this is to refactor by using replace conditional with map pattern:

const CARDS = {
  news: NewsCard,
  post: PostCard,
  user: UserCard,
};

function Card({ card }) {
  const Component = CARDS[card.type] || DefaultCard;
  return <Component card={card} />
}

This not only makes the code shorter. It also localizes the logic into a central place and makes it easier to change.
You can scan very easy what are all possible values here.

This same refactoring work for most languages, I have worked with.

Redux

This an example from Redux documentation.

function treeReducer(state, action) {
  switch (action.type) {
    case CREATE_NODE:
      return {
        id: action.nodeId,
        counter: 0,
        childIds: []
      }
    case INCREMENT:
      return {
        ...state,
        counter: state.counter + 1
      }
    case ADD_CHILD:
      return {
        ...state,
        childIds: [ ...state, action.childId ],
      }
    case REMOVE_CHILD:
      return {
        ...state,
        childIds: state.filter(id => id !== action.childId)
      }
    default:
      return state
  }
}

It illustrates how redux works quite well.

I always prefer to use a utility like redux-create-reducer, to remove the noise from here and handle things like default case.

const treeReducer = createReducer({
  [CREATE_NODE]: (state, action) => ({
    id: action.nodeId,
    counter: 0,
    childIds: []
  }),
  [INCREMENT]: (state) => ({
    ...state,
    counter: state.counter + 1,
  }),
  [ADD_CHILD]: (state, action) => ({
    ...state,
    childIds: [ ...state, action.childId ],
  }),
  [REMOVE_CHILD]: (state, action) => ({
    ...state,
    childIds: state.filter(id => id !== action.childId)
  });
});

This helps to reduce the noise and boilerplate from Redux. It also nudges people to split their reducers into smaller functions.

More complex example

The above examples are somehow more obvious.

Here is another example, which hides the logic.

Recently, I was working on a share button component. This was its initial version.

function ShareButton({ sharable, medium }) {
  const icon = medium === 'twitter' ? <IconTwitter /> : <IconFacebook />;

  const onClick = () => {
    if (medium === 'facebook') {
      shareOnFacebook(sharable)
    } else {
      shareOnTwitter(sharable)
    }
  }
  return (
    <Button
      icon={medium.icon}
      title={`Share on ${capitalize(medium)}`}
      onClick={onClick}
    />
  );
}

This isn't that bad. However, when I had to a third share medium - LinkedIn.
It wasn't very easy. I had to do multiple changes. I did the following:

const MEDIUMS = {
  twitter: {
    icon: <IconTwitter className={styles.twitter} />,
    title: 'Share on Twitter',
    share: shareOnTwitter,
  },
  facebook: {
    icon: <IconFacebook className={styles.facebook} />,
    title: 'Share on Facebook',
    share: shareOnFacebook,
  },
  linkedin: {
    icon: <IconLinkedIn className={styles.linkedIn} />,
    title: 'Share on LinkedIn',
    share: shareOnLinkedIn,
  },
};

function ShareButton({ sharable, medium }) {
  const medium = MEDIUMS[medium];

  return (
    <Button
      icon={medium.icon}
      title={medium.title}
      onClick={() => medium.share(sharable)}
    />
  );
}

If you use Flow or TypeScript you can enforce that medium can only be one of the values we support:

// TypeScript
interface IProps {
  sharable: ISharable;
  medium: keyof typeof MEDIUMS; 
}

Conclusion

This is simple and natural refactoring.

Use this refactoring when you see data being mapped to other data or behavior.

Not every switch case is the same. Some times you can't easily convert a switch or group of ifs to an object map.