1

I started with react a few months ago and I thought I understood mutations well enough until now.

I have a function that accepts an object, selected itemId, and parentId (optional). Data can have nested elements upto 1 level. What i need to do is change the value of the key selected if the itemId matches. If the itemId is inside a parent, then I need to set selected inside that object.

Another use case is, if the itemId is the id of the parent. In this case I need to set the value of selected of all the children as well. And this where the problem comes. When I am trying to set the children's selected value, the function is mutating the data that I am sending, and therefore returning wrong results.

Here is the function in question:

function returnSelectedItems(data, itemId, parentId) {
  // created this object trying to rule out mutation problem in the tests. @see tests file.
  const originalData = data.map(item => item);
  if (parentId) {
    // if parent id is present, a child element has been selected, then directly go to the element
    // go through all the items
    return originalData.map(parentItem => {
      // see if the id matches the parent item
      if (parentItem.id === parentId) {
        // if yes, check if it contains any children
        if (parentItem.children && parentItem.children.length > 0) {
          // iterate through their children and assign the resulting array as the children for the parent
          parentItem.children = parentItem.children.map(childItem => {
            // find the child with the id selected and change its value
            if (childItem.id === itemId) {
              childItem.selected = !childItem.selected;
            }
            return childItem;
          });
        }
      }
      return parentItem;
    });
  } else {
    return originalData.map(item => {
      if (item.id === itemId) {
        // set the item to the opposite it was before
        item.selected = !item.selected;

        // if it has children, we have to set the values for all of them.
        if (item.children && item.children.length > 0) {
          item.children = item.children.map(childItem => {
            childItem.selected = item.selected;
            return childItem;
          });
        }
      }
      return item;
    });
  }
}

And here are the tests that I created to test if this works as expected:

test('WITHOUT children test for selectedItems function', () => {
  const data = [
    {
      id: 1,
      name: 'test',
      selected: false
    },
    {
      id: 2,
      name: 'test',
      selected: false
    },
    {
      id: 3,
      name: 'test',
      selected: false
    },
    {
      id: 4,
      name: 'test',
      selected: false
    },
    {
      id: 5,
      name: 'test',
      selected: false
    }
  ];
  expect(returnSelectedItems(data, 4)).toEqual(
    data.map(item => {
      if (item.id === 4) {
        item.selected = true;
      }
      return item;
    })
  );
  expect(returnSelectedItems(data, 5)).toEqual(
    data.map(item => {
      if (item.id === 5) {
        item.selected = true;
      }
      return item;
    })
  );
  expect(
    returnSelectedItems(
      data.map(item => {
        if (item.id === 4) {
          item.selected = true;
        }
        return item;
      }, 4)
    )
  ).toEqual(data);
  expect(
    returnSelectedItems(
      data.map(item => {
        if (item.id === 5) {
          item.selected = true;
        }
        return item;
      }, 5)
    )
  ).toEqual(data);
});

test('WITH children test for selectedItems function', () => {
  const data = [
    {
      id: 1,
      name: 'test',
      selected: false,
      children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
    },
    {
      id: 2,
      name: 'test',
      selected: false,
      children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
    },
    {
      id: 3,
      name: 'test',
      selected: false,
      children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
    },
    {
      id: 4,
      name: 'test',
      selected: false,
      children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
    },
    {
      id: 5,
      name: 'test',
      selected: false,
      children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
    }
  ];

  const backupData = [
    {
      id: 1,
      name: 'test',
      selected: false,
      children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
    },
    {
      id: 2,
      name: 'test',
      selected: false,
      children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
    },
    {
      id: 3,
      name: 'test',
      selected: false,
      children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
    },
    {
      id: 4,
      name: 'test',
      selected: false,
      children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
    },
    {
      id: 5,
      name: 'test',
      selected: false,
      children: [{id: 1, name: 'test', selected: false}, {id: 2, name: 'test', selected: false}, {id: 3, name: 'test', selected: false}]
    }
  ];

  // this console log is correct
  console.log(data.filter(item => item.id === 4)[0]);
  const dataBack = returnSelectedItems(data, 4);
  // this console is also correct
  console.log(dataBack.filter(item => item.id === 4)[0]);
  // but here the value of the data is changed
  console.log(data.filter(item => item.id === 4)[0]);

  // this passes, but it shouldn't
  expect(returnSelectedItems(data, 4)).toEqual(
    data.map(item => {
      if (item.id === 4) {
        item.selected = true;
      }
      return item;
    })
  );

  // this should pass but it does not
  expect(backupData).toEqual(data);
});

Thanks for your help!

1 Answer 1

4

The const originalData = data.map(item => item) only creates a shallow copy of the data array. And hence whatever object item you modify in the originalData array will result in mutation of the data array. And hence you must deep copy the data array. Use a library like Lodash for convenience.

Sign up to request clarification or add additional context in comments.

1 Comment

Perfect! thanks! This solves the problem. BTW, for others having similar problems, I only imported lodash.clonedeep in my app using yarn add lodash.clonedeep. Then import cloneDeep from 'lodash.clonedeep'; const originalData = cloneDeep(data);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.