'React.useState is changing initialValues const

I'm experiencing some odd behavior with react's useState hook. I would like to know why this is happening. I can see a few ways to sidestep this behavior, but want to know whats going on.

I am initializing the state with the following const:

const initialValues = {
  order_id: '',
  postal_code: '',
  products: [
    {
      number: '',
      qty: ''
    }
  ]
}

const App = (props) => {
  const [values, setValues] = React.useState(initialValues);
...

products is an array of variable size. As the user fills in fields more appear. The change handler is:

  const handleProductChange = (key) => (field) => (e) => {
    if (e.target.value >= 0 || e.target.value == '') {
      let products = values.products;
      products[key][field] = e.target.value;
      setValues({ ...values, products });
    }
  }

What I am noticing is that if I console log initialValues, the products change when the fields are changed. None of the other fields change, only inside the array.

Here is a codepen of a working example.

How is this possible? If you look at the full codepen, you'll see that initialValues is only referenced when setting default state, and resetting it. So I don't understand why it would be trying to update that variable at all. In addition, its a const declared outside of the component, so shouldn't that not work anyway?

I attempted the following with the same result:

const initialProducts = [
  {
    number: '',
    qty: ''
  }
];

const initialValues = {
  order_id: '',
  postal_code: '',
  products: initialProducts
}

In this case, both consts were modified.

Any insight would be appreciated.



Solution 1:[1]

Probably the simplest move forward is to create a new useState for products which I had started to suspect before asking the question, but a solution to keep the logic similar to how it was before would be:

let products = values.products.map(product => ({...product}));

to create a completely new array as well as new nested objects.

As @PatrickRoberts pointed out, the products variable was not correctly creating a new array, but was continuing to point to the array reference in state, which is why it was being modified.

More explanation on the underlying reason initialValues was changed: Is JavaScript a pass-by-reference or pass-by-value language?

Solution 2:[2]

Alongside exploding state into multiple of 1 level deep you may inline your initial:

  = useState({ ... });

or wrap it into function

function getInitial() {
  return {
  ....
  };
}

// ...

 = useState(getInitial());

Both approaches will give you brand new object on each call so you will be safe.

Anyway you are responsible to decide if you need 2+ level nested state. Say I see it legit to have someone's information to be object with address been object as well(2nd level deep). Splitting state into targetPersonAddress, sourePersonAddress and whoEverElsePersonAddress just to avoid nesting looks like affecting readability to me.

Solution 3:[3]

This would be a good candidate for a custom hook. Let's call it usePureState() and allow it to be used the same as useState() except the dispatcher can accept nested objects which will immutably update the state. To implement it, we'll use useReducer() instead of useState():

const pureReduce = (oldState, newState) => (
  oldState instanceof Object
    ? Object.assign(
        Array.isArray(oldState) ? [...oldState] : { ...oldState },
        ...Object.keys(newState).map(
          key => ({ [key]: pureReduce(oldState[key], newState[key]) })
        )
      )
    : newState
);

const usePureState = initialState => (
  React.useReducer(pureReduce, initialState)
);

Then the usage would be:

const [values, setValues] = usePureState(initialValues);
...
const handleProductChange = key => field => event => {
  if (event.target.value >= 0 || event.target.value === '') {
    setValues({
      products: { [key]: { [field]: event.target.value } }
    });
  }
};

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 skyboyer
Solution 3