47

What's the best/correct way to update a nested array of data in a store using redux?

My store looks like this:

{
    items:{
        1: {
            id: 1,
            key: "value",
            links: [
                {
                    id: 10001
                    data: "some more stuff"
                },
                ...
            ]
        },
        ...
    }
}

I have a pair of asynchronous actions that updates the complete items object but I have another pair of actions that I want to update a specific links array.

My reducer currently looks like this but I'm not sure if this is the correct approach:

  switch (action.type) {
    case RESOURCE_TYPE_LINK_ADD_SUCCESS:
      // TODO: check whether the following is acceptable or should we create a new one?
      state.items[action.resourceTypeId].isSourceOf.push(action.resourceTypeLink);
      return Object.assign({}, state, {
        items: state.items,
      });
  }
Clarkie
  • 7,490
  • 9
  • 39
  • 53

2 Answers2

58

Jonny's answer is correct (never mutate the state given to you!) but I wanted to add another point to it. If all your objects have IDs, it's generally a bad idea to keep the state shape nested.

This:

{
  items: {
    1: {
      id: 1,
      links: [{
        id: 10001
      }]
    }
  }
}

is a shape that is hard to update.

It doesn't have to be this way! You can instead store it like this:

{
  items: {
    1: {
      id: 1,
      links: [10001]
    }
  },
  links: {
    10001: {
      id: 10001
    }
  }
}

This is much easier for update because there is just one canonical copy of any entity. If you need to let user “edit a link”, there is just one place where it needs to be updated—and it's completely independent of items or anything other referring to links.

To get your API responses into such a shape, you can use normalizr. Once your entities inside the server actions are normalized, you can write a simple reducer that merges them into the current state:

import merge from 'lodash/object/merge';

function entities(state = { items: {}, links: {} }, action) {
  if (action.response && action.response.entities) {
    return merge({}, state, action.response.entities);
  }

  return state;
}

Please see Redux real-world example for a demo of such approach.

Community
  • 1
  • 1
Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
  • I'm using normalizr but I also need the items in the same order they came in the API response. I was thinking on implementing a "linked list" using entities ids but I wonder if there is a better approach instead. – hisa_py Jan 26 '16 at 20:27
  • @hisa_py You'll want to keep an array of IDs separately. Both `real-world` and `shopping-cart` examples in Redux repo do that. – Dan Abramov Jan 26 '16 at 20:53
  • 1
    Thx @Dan ... Nice and simple ... Now I remember I saw it before in the examples – hisa_py Jan 26 '16 at 20:59
  • 10
    Is it always advantageous to convert nested structures to flatter structures in Redux? What if there are no entities to denormalize, just a deep tree? Something like... { items: { 12345: { a: { b: { c: { d: { info: 'hi' }}}}}}} I know you can use redux-ignore if performance is any consideration, so is there any danger in keeping data in this structure (say if the server sends it this way and I have no control)? Can't I just nest reducers as needed? – granmoe Apr 06 '16 at 02:43
50

React's update() immutability helper is a convenient way to create an updated version of a plain old JavaScript object without mutating it.

You give it the source object to be updated and an object describing paths to the pieces which need to be updated and changes that need to be made.

e.g., if an action had id and link properties and you wanted to push the link to an array of links in an item keyed with the id:

var update = require('react/lib/update')

// ...

return update(state, {
  items: {
    [action.id]: {
      links: {$push: action.link}
    }
  }
})

(Example uses an ES6 computed property name for action.id)

Jonny Buchanan
  • 61,926
  • 17
  • 143
  • 150