Ahmet Ulutaş
Ahmet Ulutaş

Reputation: 227

I can't properly change the boolean of an array with prevState

Thanks everybody trying to answer my previous question. I've just learned how to mutate boolean in my array objects.

Now I need to trigger re-render since my object changed but don't know much about life cycles. I'm newbie in react so a bit of hand would be perfect. Thanks folks.

  export default function App() {
      const [obj, setObj] = useState([
        { name: "chicken", comments: "nice", photo: img, boolean: false },
        { name: "beef", comments: "ehh", photo: img, boolean: false },
        { name: "turkey", comments: "not bad", photo: img,  boolean: false },
        { name: "broccoli", comments: "cool", photo: img,  boolean: false },
        { name: "carrot", comments: "disgusting",photo: img, boolean: false }
      ]);
    
    const handleClick = (e) => {
      const idx = e.target.value;
      setObj(prev => {
    prev[idx].boolean = !prev[idx].boolean
    return prev
      });
      window.alert(JSON.stringify(obj)); 
    
      }
              return (
              <div>
              {obj.map((map, key) => (
                <div>
                  <img src={map.photo} alt={map.key} />
                  <h1>{map.name}</h1>
                  <p>{`${map.boolean}`}</p>
                  <p>{key}</p>
                  <button value={key} onClick={handleClick}>
                    show comments
                  </button>
                </div>
              ))}
            </div>
          );
    }

Upvotes: 1

Views: 1126

Answers (4)

SoroushOwji
SoroushOwji

Reputation: 123

Well, this is actually what you are doing there, you are spreading an array and adding an item after that! what you could do:

const handleClick = (e) => {
    const idx = e.target.value;
    setObj(prev => {
       prev[idx].boolean = true;
       return prev;
      } 
    )
    console.log(obj);
  };

this would work most likely.

also for this kind of huge states, I suggest you consider useReducer from react.

If I want to complete above:

export default function App() {
  const [obj, setObj] = useState([...]);
  const [update, setUpdate] = useState(false);
  const handleClick = (e) => {
    const idx = e.target.value;
    setObj(prev => {
       prev[idx].boolean = true;
       return prev;
      } 
    )
    setUpdate(prev => !prev);
  };
  return (
    <div className="main-div">
      {obj.map((map, key) => (
        <div className="child-div">
          <img src={map.photo} alt={map.key} />
          <h1>{map.name}</h1>
          <p>{map.boolean ? map.comments : null}</p>
          <button value={key} onClick={handleClick}>
          {map.boolean ? "hide comments" : "show comments"}
          </button>
        </div>
      ))}
    </div>
  );
}

This will make it work.

Upvotes: 1

Ahmet Ulutaş
Ahmet Ulutaş

Reputation: 227

I have finally solved the problem thanks to the contributions of the commenters.

Here is the problem. I tried to change the state, which is an array of objects. I did it because prevstate works on strings and integer types of states (Increment the number button for instance.) However, things get different when mutating objects and arrays.

If you mutate an object, react wont re-render because spreading array method creates a shallow copy. Thanks to @kmp and others for helping me figure that out.

I had to create a copy of the array to mutate and let react know that state is mutated. The mistaken setStat code which works to change boolean but doesn't work to re-render is at the bottom of the code commented out.

I learned a lot from this mistake and I really appreciate the guys who contributed. Cheers.

export default function App() {
  const [obj, setObj] = useState([
    { name: "chicken", comments: "nice", photo: img, boolean: false },
    { name: "beef", comments: "ehh", photo: img, boolean: false },
    { name: "turkey", comments: "not bad", photo: img,  boolean: false },
    { name: "broccoli", comments: "cool", photo: img,  boolean: false },
    {
      name: "carrot",
      comments: "disgusting",
      photo: img,
      key: 4,
      boolean: false
    }
  ]);

  const handleClick = (e) => {
      const idx = e.target.value;
      const newObj = JSON.parse(JSON.stringify(obj)); 
      newObj[idx].boolean = !newObj[idx].boolean; 
      setObj(newObj);
  }
  return (
    <div className="main-div">
      {obj.map((map, key) => (
        <div className="child-div">
          <img src={map.photo} alt={map.key} />
          <h1>{map.name}</h1>
          <p>{map.boolean ? map.comments : null}</p>
          <button value={key} onClick={handleClick}>
          {map.boolean ? "hide comments" : "show comments"}
          </button>
        </div>
      ))}
    </div>
  );
}

/*
const idx = e.target.value;
setObj(prev => {
  prev[idx].boolean = !prev[idx].boolean;
  return prev;
})

window.alert(JSON.stringify(obj));
*/

Upvotes: 0

kmp
kmp

Reputation: 802

This is what's called a destructuring assignment

[...prev, prev[idx].boolean = true]

You basically say.. Hey, take this object, spread it's values, then assign this new value and append it. The problem with this is that you're creating a new variable prev[idx].boolean NOT assigning a new value. That's the root of the problem.

Now, what you should do is create this variable outside

const value = { ...prev[idx] }
value.boolean = true

You should create a new object otherwise it's passed by reference.

In Pass by Reference, a function is called by directly passing the reference/address of the variable as the argument. Changing the argument inside the function affects the variable passed from outside the function. In Javascript objects and arrays are passed by reference.

Then assign it

setObj(prev => 
  [...prev, value]
)

This means you will override any destructured element with the same value or if it doesn't have any, just assign the new value.


Edit

The problem is that you're mutating the state variable

setObj(prev => {
  prev[idx].boolean = !prev[idx].boolean
  return prev
});

Do this instead

setObj(prev => {
  const value = { ...prev[idx] };
  value.boolean = !value.boolean;
  
  return [...prev, value];
});

Or this

const value = { ...prev[idx] };
value.boolean = !value.boolean;

setObj([...obj, value]);

In my previous response I've quoted pass by reference.. you should read it carefully, because you're trying to create a shallow copy of an object which wouldn't be a problem if you didn't care about changing the original object as well.

Since you're trying to change the state, you CANNOT change the state variable itself. This is what's happening here

prev[idx].boolean = !prev[idx].boolean
return prev

You have to create a new object to trigger the re-render lifecycle.

Upvotes: 1

Prem
Prem

Reputation: 71

The issue is here

setObj(prev => 
      [...prev, prev[idx].boolean = true]
)

You're actually pushing another boolean value modify handleClick function as such

const handleClick = (e) => {
    const idx = e.target.value;// getting the required idx
    const newObj = JSON.parse(JSON.stringify(obj)); // creating a new obj 
    obj[idx].boolean = true; // setting the newobj boolean value
    setObj(newObj); // useState function to set new state.
};

Upvotes: 1

Related Questions