35

Given my initial redux state is :

const state = {
  currentView: 'ROOMS_VIEW',
  navbarLinks: List([
    {name: 'Rooms', key: 'ROOMS_VIEW'},
    {name: 'Dev', key: ''}
  ]),
  roomListsSelected: {group: 0, item: 0},
  roomLists: [
    {
      name: "Filters",
      expanded: true,
      listItems: [
        { icon: 'images/icon-warning.svg', name: 'Alerts', filter: room => room.hasAlert },
        { icon: 'images/icon-playlist.svg', name: 'In Progress', filter: room => room.progress > 20 },
        { icon: 'images/icon-playlist.svg', name: 'Almost Done', filter: room => room.progress > 90 },
        { icon: 'images/icon-playlist.svg', name: 'Complete', filter: room => room.status === 'complete' },
        { icon: 'images/icon-playlist.svg', name: 'Recently Completed', filter: room => false },
        { icon: 'images/icon-playlist.svg', name: 'All Rooms', filter: room => true }
      ]
    }
  ],
  rooms: List(generateRooms())
}

I need to make a reducer that does this:

state.roomList[n].expanded = !state.roomList[n].expanded

I am new to using a Redux workflow and the best way to solve this is to make roomList an immutable.js object or write some code to make a deep clone of my state object.

Also state.roomList will have new data pushed to it from future features.

Summery / Question: What is the best way to return a new state object in a reducer when making changes like this deep in the state, or should I change the structure of the Redux state object?

What I did In the end Immutable seems the way to go. There are some tricks with Immutable to reduce react rendering time and it meets all the project requirements. Also it is early enough in the project to use a new library without making major changes.

Steven Bayer
  • 1,377
  • 2
  • 11
  • 16

2 Answers2

61

First, idiomatic Redux encourages you to "normalize" your state and flatten it as much as possible. Use objects keyed by item IDs to allow direct lookups of items, use arrays of IDs to denote ordering, and anywhere that one item needs to refer to another, it only stores the ID of the other item instead of the actual data. That allows you to do simpler lookups and updates of nested objects. See the Redux FAQ question on nested data.

Also, it looks like you're currently storing a number of functions directly in your Redux state. Technically that works, but it's definitely not idiomatic, and will break features like time-travel debugging, so it's heavily discouraged. The Redux FAQ gives some more info on why storing non-serializable values in your Redux state is a bad idea.

edit:

As a follow-up, I recently added a new section to the Redux docs, on the topic of "Structuring Reducers". In particular, this section includes chapters on "Normalizing State Shape" and "Updating Normalized Data", as well as "Immutable Update Patterns".

markerikson
  • 42,022
  • 7
  • 87
  • 109
  • 1
    This seems like the most 'correct' answer and storing literal functions in state was a quick fix that will be change to a more traditional method before release. – Steven Bayer Jun 23 '16 at 17:07
  • Sure. One related approach you may look at is storing some kind of filter name or identifier in your state, and then using that as a key to look up the actual filter function in a component or a selector, similar to the approach suggested for managing dialog components at http://stackoverflow.com/questions/35623656/how-can-i-display-a-modal-dialog-in-redux-that-performs-asynchronous-actions/ . – markerikson Jun 23 '16 at 17:43
  • 1
    Examples would be nice – Green Nov 08 '16 at 22:15
  • All links are broken. – transformerTroy Mar 03 '18 at 23:31
  • We did recently update the docs build tool, which changed URLs. It's supposed to redirect the old URLs, but that seems to not be working right. I'll update the links. – markerikson Mar 03 '18 at 23:54
8

Reducer composition: De-compose your reducers into smaller pieces so that a reducer is small enough to deal with simple data structure. eg. In your case you may have: roomListReducer listItemsReducer listItemReducer. Then at each reducer, its going to make it much more easier for you to read which part of the state you are dealing with. It helps a lot because each of your reducer is dealing with small piece of data that you don't have to worry things like 'should i deep copy or shallow copy'.

Immutable I personally don't use immutable.js because I prefer to deal with plain objects. and there are just too much code to change to adopt a new API. But the idea is, make sure your state changes are always done through pure functions. Therefore, you can just simply write your own helper functions to do what you want, just make sure that they are tested thoroughly when dealing with complex objects.

Or simply enough, you can always deep copy your state in each reducer, and mutate in the copy then return the copy. But this is obviously not the best way.

xiaofan2406
  • 3,042
  • 3
  • 22
  • 33
  • @StevenBayer did i miss something important? – xiaofan2406 Jun 23 '16 at 01:08
  • 1
    With De-composing my reducers I still end up with complex objects they are just slightly less nested. Nothing important missed, just a helpful link incase someone else googles this issue. – Steven Bayer Jun 23 '16 at 01:12
  • 3
    @StevenBayer your state tree will remain the same. But at each reducer, you will be dealing with simpler data structure. Sorry I wasn't clear in the post. Also, you don't always need `combineReducer`. Reducers are just functions that takes `state` and `action` as parameters. You can call another reducer function in a reducer function to reduce the responsibilities of each reducer. – xiaofan2406 Jun 23 '16 at 01:18
  • 1
    I'm not sure how I would implement a sub-reducer on `state.roomLists` or even more so for `state.roomLists[n].listItems[n]`. Also wouldn't the sun functionality of the sub-reducers add up to a deep clone? – Steven Bayer Jun 23 '16 at 01:41