1

I followed the answer in this thread to try to update my deeply nested object in React. React: Setting State for Deeply Nested Objects w/ Hooks

What seems to work like a charm there, will somehow break for me when doing the following:

I have a table populated with items from an array defined like so:

const [items, setItems] = useState([
{
  selected: false,
  title: 'Item 1',
  status: 'new'
},
{
  selected: false,
  title: 'Item 2',
  status: 'used'
},
]);

When selecting an item from that list this function gets called to update selected variable for the object with the index i like so:

const select = (e) => {
  const i = e.target.getAttribute('data-index');
  setItems((prevState) => {
    prevState[i].selected = !prevState[i].selected;
    return [...prevState];
  });
};

This will work exactly once. If I trigger select a second time or any time after that return [...prevState] somehow keeps returning the state unchanged. (selected stays true forever). I can't solve this.

items is attached to a component List like so:

<List
   items={items}
/>

and inside List (shortened code):

{items.map((item, i) => {
      return (
        <tr className="list-table-tr">
          {hasSelector ? (
            <td className="list-table-td-selector">
              {item.selected ? (
                <div
                  data-index={i}
                  className="global-selector-selected"
                  onClick={select}
                ></div>
              ) : (
                <div
                  data-index={i}
                  className="global-selector-unselected"
                  onClick={select}
                ></div>
              )}
            </td>
          ) : null}
smac89
  • 39,374
  • 15
  • 132
  • 179
Dawesign
  • 643
  • 1
  • 7
  • 25

1 Answers1

8

You're breaking one of the primary rules of React state: You're modifying a state object directly, rather than making a copy.

To correctly do the update, you'd do this:

const select = (e) => {
    const i = e.target.getAttribute('data-index');
    setItems((prevState) => {
        // Copy the array (your code was doing that)
        const update = [...prevState];
        const item = update[i];
        // Copy the object (your code wasn't doing that) and update its
        // `selected` property
        update[i] = {...item, selected: !item.selected};
        return update;
    });
};

Note how both the array and the object are copied, rather than just the array.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • This works! I am now wondering, why the answer on the linked question is the accepted one eventhough they are also directly modifying the state directly. Anyway, thanks a ton for relieving me from my 2 hour long headache :) – Dawesign Sep 21 '21 at 14:53
  • Why make a copy of the entire state when only `prevState[i]` needs to be updated? – smac89 Sep 21 '21 at 14:57
  • Also `select` should take an element, not a number – smac89 Sep 21 '21 at 14:57
  • @smac89 - Because that's the way React works. The state member itself (`items`) has to change for React to reliably render the changed item. The `select` that was in the answer when you posted that comment is what was in the question when I answered it. The OP has since changed it. – T.J. Crowder Sep 21 '21 at 16:03
  • @Pixell - The accepted answer there is, unfortunately, incorrect. It will seem to work in some cases because it's making a copy of the top-level object, but it needs to copy more than that and will not work reliably otherwise. I've [posted an answer there](https://stackoverflow.com/a/69272573/157247) explaining that. – T.J. Crowder Sep 21 '21 at 16:43