How to cut the boilerplate for nested Redux state

So you have this nested structure in your Redux state:

{
  users: [
    { 
      id: 1, name: "Jakob", 
      comments: [
        { id: 1, message: "hello there" }, 
        { id: 2, message:"hi!" }
      ]
    }, 
    { 
      id: 2, name: "Johanna", 
      comments: [ 
        { id: 1, message: "yo" }, 
        { id: 2, message: "sup?" }
      ]
    } 
  ] 
}; 

It’s a list of users, and each user can have many comments.

Your task is to create a reducer that modifies a comment for a user.

You know you should NOT mutate the Redux state… but it’s so tempting. You can see the solution right before your eyes. All you need to do is:

state.user[id].comments[commentIndex] = "this is my latest version of a comment!";

That’s intuitive, quick and easy – but also so very wrong. You know mutating state in a reducer can lead to unexpected bugs

So you get back to reading the Redux doc to figure out the best practice way to do it. This time you read the docs very carefully. You even read the troubleshooting part.

After a very frustrating hour, you come up with code that looks like this:

case 'UPDATE_COMMENT':
  return {
    ...state,
      users: [
        ...state.users.slice(0, action.userId),
        Object.assign({}, state.users[action.id], {
          comments: [...state.users[action.id].comments, action.comment]
        }),
        ...state.users.slice(action.userId + 1)
      ] 
} 

WTF!?

It was a pain to write that code.

And you are p-r-e-t-t-y sure that code will give you a headache in 2 months when you will touch it next time.

There got to be a faster way to work with nested data in Redux than this.

And there is! (what a surprise, right? 🙂 )

The solution is to normalize the state. It means to structure the data in a way that doesn’t require you to go “deep”. Look at this way of structuring the state:

{
  users: {
    1: {name: "Jakob", comments: [1,2] }
    2: {name: "Johanna", comments: [3,4]}
  },
  comments: {
    1: "hello there",
    2: "hi!",
    3: "yo",
    4: "sup?"
  }
}

Note that instead of nesting comments inside users, we put the comments on the root level instead. Also, we no longer use lists, but we use objects with ids which is much easier to work with.

When we structure our data like that, we can write a commentsReducer that updates a comment like this:

case 'UPDATE_COMMENT':
  return {
    ...state,
    [action.comment.id]: action.comment.text
}

ahh.. that looks much better!