darKnight
darKnight

Reputation: 6481

Spread Array of Objects in JavaScript

I have a react-redux application, and I have a reducer named dataReducer that has a default state like this:

const defaultState = {
  isLoading: false,
  data: [{
    id: 1,
    label: 'abc',
    elements: [{ color: 'red', id: 1}],
  }],
};

One of the reducers adds elements to data in the defaultState. I need to test this reducer by passing the payload and then validating the new state. I want to use the spread operator to build the new state from the old defaultState, but I am having some trouble achieving it. I tried the following, but it's not working:

const newElement = {
  colour: 'blue',
  id: 1
};

const newState = [
  {
    ...defaultState.data[0],
    elements: [{
      ...defaultState.data[0].elements,
      ...newElement,
    }]
  }
];

expect(dataReducer(defaultState, action)).toEqual(newState); // returns false

It would be great if I could somehow avoid using array index (defaultState.data[0]) as there might be multiple objects in the defaultState array in the real application, though for the purpose of testing, I am keeping just one object to keep things simple.

Upvotes: 2

Views: 162

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074475

If you're adding to the end, you spread out the other aspects of state in the new state object, then override data with the current contents of it followed by the new entry:

const newState = {               // New state is an object, not an aray
  ...defaultState,               // Get everything from defaultState
  data: [                        // Replace `data` array with a new array
    {
      ...defaultState.data[0],   // with the first item's contents
      elements: [                // updating its `elements` array
        ...defaultState.data[0].elements,
        newElement
      ]
    },
    ...defaultState.data.slice(1) // include any after the first (none in your example)
  ]
};

Live Example:

const defaultState = {
  isLoading: false,
  data: [{
    id: 1,
    label: 'abc',
    elements: [{ color: 'red', id: 1}],
  }],
};

const newElement = {
  colour: 'blue',
  id: 1
};

const newState = {               // New state is an object, not an aray
  ...defaultState,               // Get everything from defaultState
  data: [                        // Replace `data` array with a new array
    {
      ...defaultState.data[0],   // with the first item's contents
      elements: [                // updating its `elements` array
        ...defaultState.data[0].elements,
        newElement
      ]
    },
    ...defaultState.data.slice(1) // include any after the first (none in your example)
  ]
};

console.log(newState);
.as-console-wrapper {
    max-height: 100% !important;
}

There's no getting around specifying the entry in data that you want (e.g., data[0]).


In a comment you've asked how to handle this:

Let's say data (present inside defaultState) has multiple objects entries in it. First object has id of 1, second one has id of 2. Now the newElement to be added has an id of 2. So the newElement should get added to the second object. Where in second object? Inside the elements property of the second object. The addition should not over-write existing entries in the elements array.

You'll need to find the index of the entry in data:

const index = defaultState.data.findIndex(({id}) => id === newElement.id);

I'm going to assume you know that will always find something (so it won't return -1). To then apply that index to the code above, you'd do this:

const newState = {                        // New state is an object, not an aray
  ...defaultState,                        // Get everything from defaultState
  data: [                                 // Replace `data` array with a new array
    ...defaultState.data.slice(0, index), // Include all entries prior to the one we're modifying
    {
      ...defaultState.data[index],        // Include the entry we're modifying...
      elements: [                         // ...updating its `elements` array
        ...defaultState.data[index].elements,
        newElement
      ]
    },
    ...defaultState.data.slice(index + 1) // include any after the one we're updating
  ]
};

The only real change there is adding the ...defaultState.data.slice(0, index) at the beginning of the new data, and using index instead of 0.

Live Example:

const defaultState = {
  isLoading: false,
  data: [
      {
        id: 1,
        label: 'abc',
        elements: [{ color: 'red', id: 1}],
      },
      {
        id: 2,
        label: 'def',
        elements: [{ color: 'green', id: 2}],
      },
      {
        id: 3,
        label: 'ghi',
        elements: [{ color: 'yellow', id: 3}],
      }
  ],
};

const newElement = {
  colour: 'blue',
  id: 2
};

const index = defaultState.data.findIndex(({id}) => id === newElement.id);

const newState = {                        // New state is an object, not an aray
  ...defaultState,                        // Get everything from defaultState
  data: [                                 // Replace `data` array with a new array
    ...defaultState.data.slice(0, index), // Include all entries prior to the one we're modifying
    {
      ...defaultState.data[index],        // Include the entry we're modifying...
      elements: [                         // ...updating its `elements` array
        ...defaultState.data[index].elements,
        newElement
      ]
    },
    ...defaultState.data.slice(index + 1) // include any after the one we're updating
  ]
};

console.log(newState);
.as-console-wrapper {
    max-height: 100% !important;
}

Upvotes: 1

Related Questions