0

I have the following code in a React project:

changeChecked (task){
    this.setState(prevState => {
      let copy = [...prevState.tasks];
      copy.forEach((item, index) => {
        if (item == task) {
          copy[index].checked = !copy[index].checked;
        }
        console.log(item);    
        // {name: "Task A", checked : true}
        // {name: "Task B", checked : false}
      });
      console.log(copy);
      // [
      //      {name : "Task A", checked : false},
      //      {name : "Task B", checked : false}
      // ]
      return {tasks : copy}
    });
 }

the initial state of the "this" (a React component) is this:

{
  tasks : [
    {name : "Task A", checked : false},
    {name : "Task B", checked : false}
  ]
}

When I call that function (by button click) with the first task in the state.tasks as parameter, the forEach loop seems to change the item (see commented lines), but afterwards the "copy" array is unchanged. How so? When I execute this exact logic in an interactive javascript interpreter, the copy does have this first task changed.

enter image description here

EDIT: Now I see something very weird. When I expand the printed out object (line 8) in the console by clicking on it, I get this:

enter image description here

Console sais checked is true, but in the expanded view of the object it sais checked is false. What is going on here?

Washbear
  • 111
  • 8
  • Your current *condition* is incorrect, `item` refers to the object element of the array, and *objects are passed by reference*, so their equality check is always `false` even if they seem the same to the human eye, under the hood of JavaScript they are not. Ex: `const objA = { name: 'same' }` ; `const objB = { name: 'same' }` ; `objA === objB` returns `false`. – Aleksandar Dec 23 '22 at 20:15
  • @Aleksandar But I am passing the reference, not a newly created object with the same values. Check my commented lines again: The condition is met for the first item, that's why it logs // {name: "Task A", checked : true} // . – Washbear Dec 25 '22 at 10:05

3 Answers3

1

I haven't posted on SO so I can't comment on Filip Górczyński's thread but it seems like the copy should get updated if you have the conditional set correctly like they stated. I didn't test this in React as I haven't used class components in forever but doing this:

const tasks = [
    { name: "Task A", checked: false },
    { name: "Task B", checked: false }
]

let copy = [...tasks];
copy.forEach((item, index) => {
    if (item.name == "Task A") {
        copy[index].checked = !copy[index].checked;
    }
    console.log(index, item);
});
console.log(copy);

gives this in the console:

0 { name: 'Task A', checked: true }
1 { name: 'Task B', checked: false }
[
  { name: 'Task A', checked: true },
  { name: 'Task B', checked: false }
]

I think if you were to either use JSON.stringify like Dream Bold said so you can compare the objects as strings or maybe adding another property to tasks for comparison would make it work as your logic seems to check out otherwise.

Mint Jam
  • 31
  • 5
  • Following your suggestion, logging the array with stringify, it shows the desired modified array! It is still very strange to me why those two lines produce a different output though: console.log(JSON.stringify(copy)); console.log(copy); First one give a JSON string with "checked : true", second one gives array with both tasks at "checked : false". – Washbear Dec 25 '22 at 10:35
  • 1
    Can you add your updated code here or edit and add it to your question? I'm curious would be easier to visualize/test. Glad it's working for you, though! Also doing a quick google search for js object comparison I found this SO post that has a lot of info and other suggestions: https://stackoverflow.com/questions/1068834/object-comparison-in-javascript – Mint Jam Dec 25 '22 at 17:09
  • Lodash has exactly what you need (posted in comments of OP). example usage here: https://www.geeksforgeeks.org/lodash-_-isequal-method/ just throw the two objects in there and it should work. – Mint Jam Dec 25 '22 at 17:23
0

I suppose it's because you're trying to compare two objects, similarly to such case:

let task = {name: "Task A", checked: false};
let item = {name: "Task A", checked: false};
task == item;  // results to false

so in your case, condition if (item == task) { will always result to false. One of the idea would be to compare some key parts of the task/item attributes like name, or provide some unique identifier.

  • I did check that, it does result to true on the first object. So I do get into the if statement's execution block, that's why the console logs the first object with checked : true – Washbear Dec 23 '22 at 19:56
0

I solved the problem now with the following code:

changeChecked (task) {
  this.setState(prevState => {
    let copy = JSON.parse(JSON.stringify(prevState.tasks)); 
    copy.forEach((item, index) => {
      if (item.id == task.id) {
        copy[index].checked = !copy[index].checked;
      }
    });
    return copy;
  });
}

I believe the issue is that, since the spread operator [...prevState.tasks] only makes a shallow copy of the tasks array, the actual objects within are being copied by reference. As per REACT docs, you should never mutate state or prevState within a setState call, which is what I am doing here. By serialalizing and deserializing from/to JSON I am creating an actual Copy, so an array with new Objects, which I can now safely mutate since I'm not working on prevState anymore. A little necessary tweak is the condition: since I have a new object (with a new reference), I can't compare (item == task) directly, so I created a unique ID for Task objects which I use to compare.

Washbear
  • 111
  • 8