1

I'm looking to add a new item object to a category in a reducer. The reducer receives a category index and a new item object.

Does anyone know the best way to update the state immutably with this data structure:

const initialState = {    
    categories: [
        {
            id: 1,
            name: "vegetables",
            items: [
                {name: "potatoes", id: Math.floor(Math.random() * 99999)},
                {name: "carrots", id: Math.floor(Math.random() * 99999)}
            ] 
        },
        {
            id: 2,
            name: "dairy",
            items: [
                {name: "milk", id: Math.floor(Math.random() * 99999)},
                {name: "cheese", id: Math.floor(Math.random() * 99999)}
            ] 
        },
        {
            id: 3,
            name: "meat",
            items: [
                {name: "chicken", id: Math.floor(Math.random() * 99999)}
            ] 
        }
    ]
}

Or is it best to use an external package, such as immutable.js?

There are many other similar questions on stackoverflow but none that have the same structure.

Update

The rest of the reducer looks like:

const shoppingItemsReducer = (state = initialState, action) => {

    switch (action.type) {
        case ADD_SHOPPING_ITEM:

        const categories = [...state.categories];


     categories[action.selectedCategoryIndex].items.push(action.newItemObj);

            return {
                ...state,
                categories
            }
        default:
            return state
    }

}

Using push works fine but it's mutating the state

Chicken Dinner
  • 98
  • 1
  • 3
  • 9

2 Answers2

2

You can do the following without using push

const initialState = {    
    categories: [
        {
            id: 1,
            name: "vegetables",
            items: [
                {name: "potatoes", id: Math.floor(Math.random() * 99999)},
                {name: "carrots", id: Math.floor(Math.random() * 99999)}
            ] 
        },
        {
            id: 2,
            name: "dairy",
            items: [
                {name: "milk", id: Math.floor(Math.random() * 99999)},
                {name: "cheese", id: Math.floor(Math.random() * 99999)}
            ] 
        },
        {
            id: 3,
            name: "meat",
            items: [
                {name: "chicken", id: Math.floor(Math.random() * 99999)}
            ] 
        }
    ]
}

const categoryId = 2; // categoy want to update
cosnt newItem = {name: "Butter", id: Math.floor(Math.random() * 99999)}

const newState = {
  ...initialState, // or state
  categories: initialState.categories.map(category => {
    if(category.id === categoryId) {
      return {
        ...category,
        items: [
          ...category.items,
          newItem
        ]
      }
    }
    return category;
  )
}
Hafeez Hamza
  • 794
  • 3
  • 16
0

Variables containing primitive types will always point to the actual value. So if you pass it to another variable, the other variable will get a fresh copy of that value.

However, Objects and Arrays are always passed by reference. So if you were to pass an Object or Array to another variable, they will both refer to the same original object. If you were to modify any of those variables referencing the original, it would also modify the original Object/Array.

To circumvent this, you'd have to create a new copy of the Array. You can do this in plain javascript like so:

const initialState = [
    {
        id: 1,
        name: "category 1",
        items: [
            {name: "item 1", id: 1},
            {name: "item 2", id: 2}
        ] 
    },
    {
        id: 2,
        name: "category 2",
        items: [
            {name: "item 3", id: 3},
            {name: "item 4", id: 4}
        ] 
    },
    {
        id: 3,
        name: "category 3",
        items: [
            {name: "item 5", id: 5},
            {name: "item 6", id: 6}
        ] 
    }
]


const newState = [...initialState, newDataObject]

newState is a newly created array with copies of initialState with newDataObject pushed to the last index of the newState array.

EDIT: I saw you updated your question with your redux reducer. You're currently returning an object that references the initialstate:

    return {
        ...state,
        categories
    }

It should instead be returning a new object with catagories pushed up to it. You can use es6's Object.assign() to merge two object, and it will return a completely new Object containing both.

Change your return statement to:

        return Object.assign({}, state, categories)
Magnum
  • 2,299
  • 4
  • 17
  • 23
  • Is 'push' still an ok function to use to merge the new item into the array? Everywhere I've read about immutability this is frowned upon. How would this work differently with a spread operator? – Chicken Dinner Oct 25 '18 at 15:11
  • Push will mutate the array of which you are pushing to (not immutable, and will break your reducer). You should avoid using push in your reducers, unless the array you're pushing to IS a new array. The spread operator will not mutate your original, but return a copy of the original data in a new array. – Magnum Oct 25 '18 at 16:23
  • Note that the spread operator will only go one level deep. Spread is not suitable for copying multidimensional arrays with spread syntax. You would have to recursively spread on each nested object (not an easy solution, but there are implementations out there). If you need this, I would suggest taking a look at ImmutableJS for your implementation. – Magnum Oct 25 '18 at 16:23