224

I've initialized a state that is an array, and when I update it my component does not re-render. Here is a minimal proof-of-concept:

function App() {
  const [numbers, setNumbers] = React.useState([0, 1, 2, 3]);
  console.log("rendering...");
  return (
    <div className="App">
      {numbers.map(number => (
        <p>{number}</p>
      ))}
      <input
        type="text"
        value={numbers[0].toString()}
        onChange={newText => {
          let old = numbers;
          old[0] = 1;
          setNumbers(old);
        }}
      />
    </div>
  );
}

Based on this code, it seems that the input should contain the number 0 to start, and any time it is changed, the state should change too. After entering "02" in the input, the App component does not re-render. However, if I add a setTimeout in the onChange function which executes after 5 seconds, it shows that numbers has indeed been updated.

Any thoughts on why the component doesn't update?

Here is a CodeSandbox with the proof of concept.

Antony Hatchkins
  • 31,947
  • 10
  • 111
  • 111
Ryan Z
  • 2,657
  • 2
  • 8
  • 20

9 Answers9

545

You're calling setNumbers and passing it the array it already has. You've changed one of its values but it's still the same array, and I suspect React doesn't see any reason to re-render because state hasn't changed; the new array is the old array.

One easy way to avoid this is by spreading the array into a new array:

setNumbers([...old])
ray
  • 26,557
  • 5
  • 28
  • 27
  • 5
    ohhhh so it internally compares the reference instead of checking for full equality. makes sense. would be nice if we could decide rerenders ourselfs instead of letting state setters do it in the background (maybe its already possible i have no idea) – Alan Oct 12 '20 at 13:28
  • 1
    What about child component? – digz6666 Apr 06 '21 at 16:03
  • Are there any performance impacts to this solution? – ripytide Jun 02 '21 at 16:03
  • This was my problem... but why would it behave like this? I'm changing a top-level string property. Even a shallow equal check would see a different object.... – adamdabb Aug 15 '21 at 17:49
  • 1
    @adamdabb It's not checking the _contents_ of the array, it's testing whether the array itself is the same object. And even if it did test the contents you'd get the same result because you'd be checking it against itself. – ray Aug 17 '21 at 16:47
  • I understand that this will work because the state updater is always passed a new reference, but why not fix the actual state mutation causing the problem? This fixes the symptom, but really just hides the true bug. – Brian Thompson Oct 25 '21 at 19:26
  • I have the same problem with boolean. have any Idea?! – kamiyar Dec 13 '21 at 13:06
  • Is the boolean a property in an object? Same thing could occur. – ray Dec 13 '21 at 15:30
  • `setNumbers([...old])` doesn't actually set the first element to `1` however. It's pretty pointless on its own. – Bergi Jan 02 '23 at 02:55
56

You need to copy numbers like so let old = [...numbers];

useState doesn't update the value only if it has changed so if it was 44 and it became 7 it will update. but how can it know if an array or object have changed. it's by reference so when you do let old = numbers you are just passing a reference and not creating a new one

evgeni fotia
  • 4,650
  • 3
  • 16
  • 34
35

You can change state like this

const [state, setState] = ({})
setState({...state})

or if your state is Array you can change like this

const [state, setState] = ([])
setState([...state])
Dharman
  • 30,962
  • 25
  • 85
  • 135
33

Others have already given the technical solution. To anyone confused as to why this happens, is because setSomething() only re renders the component if and only if the previous and current state is different. Since arrays in javascript are reference types, if you edit an item of an array in js, it still doesn't change the reference to the original array. In js's eyes, these two arrays are the same, even though the original content inside those arrays are different. That's why setSomething() fails do detect the changes made to the old array.

Note that if you use class components and update the state using setState() then the component will always update regardless of whether the state has changed or not. So, you can change your functional component to a class component as a solution. Or follow the answers provided by others.

KMA Badshah
  • 895
  • 8
  • 16
  • Ok, so regardless of array vs object since both are references to the original, why not just always do an `Object.assign([], original)` when updating properties of a state object? Why is that not the de-facto answer and in the documentation? – AlxVallejo May 03 '22 at 12:43
0

I was working on an array that had objects in it and I tried few of the above.

My useState is :

const [options, setOptions] = useState([
{ sno: "1", text: "" },
{ sno: "2", text: "" },
{ sno: "3", text: "" },
{ sno: "4", text: "" },
]);

Now I want to add more options with blank field on a click of a button I will use the following way to achieve my purpose:

<button
    onClick={() => {
      setOptions([...options, { sno: newArray.length + 1, text: "" }]);
    }}
  >

This solved my problem and I was able to re render the component and added an object to the array.

Ashish sah
  • 755
  • 9
  • 17
-1

introduces an array of the component that is not the one of the hook. for instance:

const [numbers, setNumbers] = useState([0, 1, 2, 3]);

var numbersModify = []; //the value you want

and at the end:

setNumbers(numbersModify)

modify this numbersModify, when the hook refreshes it will return to 0 numbersModify and the hook will keep the state. Therefore, the problem of not seeing the changes will be eliminated.

:D

Carlos Noé
  • 103
  • 3
  • 11
  • This is bad practice, it introduces an unnecessary variable and an array spread should be used – Ryan Z Oct 19 '22 at 16:25
-2
  //define state using useState hook
  const [numbers, setNumbers] = React.useState([0, 1, 2, 3]);

  //copy existing numbers in temp
  let tempNumbers = [...numbers];
  // modify/add no
  tempNumbers.push(4);
  tempNumbers[0] = 10;
  // set modified numbers
  setNumbers(tempNumbers);
  • 5
    Don't simply post a snippet of code. Rather try to give a bit more context/explanation please. – kissu Jun 22 '21 at 14:42
-3

I dont have any proud of this but it works

anotherList = something
setSomething([])
setTimeout(()=>{ setSomething(anotherList)},0)
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Prathamesh More Aug 27 '22 at 10:43
  • This will cause an un-necessary state update – Ryan Z Sep 04 '22 at 22:04
-4

useState is a React hook which provides functionality of having State in a functional component. Usually it informs React to re-render the component whenever there is change in useState variables.

{
      let old = numbers;
      old[0] = 1;
      setNumbers(old);
}

In the above code since you are referring to the same variable it stores the reference not the value hence React doesn't know about the latest changes as the reference is same as previous.

To overcome use the below hack, which will not copy the reference instead it's a deep copy(copies the values)

{
      let old = JSON.parse(JSON.stringify(numbers));
      old[0] = 1;
      setNumbers(old);
}

Happy coding :)

ajaykumar mp
  • 487
  • 5
  • 12