I think I'm missing something fairly basic about useState() hook in ReactJS?

Viewed 115

I am attempting to add the same array of objects to two different stateful variables using useState(), so that I can allow the user to apply changes to the first, and then discard changes and use the second to revert to the beginning state by selecting a button. I'm new to React and the result is not working out as expected! When the user types in changes, for some reason these are simultaneously applied to both, so that there is now nothing that retains the beginning state and that can be reverted to if the use selects the 'Discard Changes' button.

I've simplified the code to the following example:

import {useState} from 'react';
  
const budgetData = 
      [ 
        {   index: 0, category: 'Housing',   item: 'Mortgage',  amount: 650.99 },
        {   index: 1, category: 'Housing',   item: 'Insurance', amount: 275.50 }, 
        {   index: 2, category: 'Utilities', item: 'Hydro',     amount:  70.00 }      
      ];

function UpdateBudget()  {
    const backup = budgetData;
    const [data, setData ] = useState(budgetData); 
    const [dataBackup    ] = useState(backup); 

    const handleChange = ( (e, row) => {
      let selectedData = [...data];
      const {name, value} = e.target;
      selectedData[row][name] = value;
      setData(selectedData);
    });

    const handleReset = ( (e) => {
    setData(dataBackup);
    });

    return  (
      <div>
        <table>
          <thead>
            <tr>
              <th>Category</th> <th>Item</th> <th>Amount</th>
            </tr>
          </thead>

          <tbody>
              { data.map( (row) =>  (
                  <tr key={row.index}>
                    <td> <input value={row.category} name="category" onChange={(e) => handleChange(e, row.index)}/> </td>
                    <td> <input value={row.item    } name="item"     onChange={(e) => handleChange(e, row.index)}/> </td>
                    <td> <input value={row.amount  } name="amount"   onChange={(e) => handleChange(e, row.index)}/> </td>
                  </tr>
                ))
              }
          </tbody>
        </table>
        <div> 
            <button onClick={ (e) => handleReset (e)}> Discard Changes </button>
        </div>
        <div style={{ marginTop: 20, fontSize: 10 }}> * data       {data.length}       * {JSON.stringify(data)}       </div> 
        <div style={{ marginTop: 20, fontSize: 10 }}> * dataBackup {dataBackup.length} * {JSON.stringify(dataBackup)} </div>
      </div>
    );
}

Grateful for someone to point me to point to what I'm missing here!

1 Answers

I have a suspicion its because cloning the array doesn't create a copy of the child elements. selectedData[row][name] this is still referencing the same value, it's a JS quirk.

const budgetData = [
  ["foo"],
  ["bar"],
  ["baz"],
]
const backup = budgetData

const clonedData = [...backup]

clonedData[0][0] = "abc"

console.log(backup) // backup data is mutated

Take a look at lodash cloneDeep which will also copy child values https://lodash.com/docs/4.17.15#cloneDeep

const budgetData = [
  ["foo"],
  ["bar"],
  ["baz"],
]
const backup = budgetData

const clonedData = _.cloneDeep(backup)

clonedData[0][0] = "abc"

console.log(backup) // backup data is not mutated
console.log(clonedData) // cloned data holds the changes
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>

Also I don't think you need the second state as you're not altering it. You can probably ditch this const [dataBackup ] = useState(backup) and just use backup

Related