48

Here's the issue: I'm trying to call 2 functions on a button click. Both functions update the state (I'm using the useState hook). First function updates value1 correctly to 'new 1', but after 1s (setTimeout) second function fires, and it changes value 2 to 'new 2' BUT! It set's value1 back to '1'. Why is this happening? Thanks in advance!

import React, { useState } from "react";

const Test = () => {
  const [state, setState] = useState({
    value1: "1",
    value2: "2"
  });

  const changeValue1 = () => {
    setState({ ...state, value1: "new 1" });
  };
  const changeValue2 = () => {
    setState({ ...state, value2: "new 2" });
  };

  return (
    <>
      <button
        onClick={() => {
          changeValue1();
          setTimeout(changeValue2, 1000);
        }}
      >
        CHANGE BOTH
      </button>
      <h1>{state.value1}</h1>
      <h1>{state.value2}</h1>
    </>
  );
};

export default Test;
Jee Mok
  • 6,157
  • 8
  • 47
  • 80
Bartek
  • 483
  • 1
  • 4
  • 5

4 Answers4

58

Welcome to the closure hell. This issue happens because whenever setState is called, state gets a new memory reference, but the functions changeValue1 and changeValue2, because of closure, keep the old initial state reference.

A solution to ensure the setState from changeValue1 and changeValue2 gets the latest state is by using a callback (having the previous state as a parameter):

import React, { useState } from "react";

const Test = () => {
  const [state, setState] = useState({
    value1: "1",
    value2: "2"
  });

  const changeValue1 = () => {
    setState((prevState) => ({ ...prevState, value1: "new 1" }));
  };
  const changeValue2 = () => {
    setState((prevState) => ({ ...prevState, value2: "new 2" }));
  };

  // ...
};

You can find a broader discussion about this closure issue here and here.

Alberto Trindade Tavares
  • 10,056
  • 5
  • 38
  • 46
  • A callback with useState hook seems to be an [undocumented](https://reactjs.org/docs/hooks-state.html) feature, are you sure that works? – HMR Oct 02 '19 at 07:32
  • @HMR Yes, it works and it's documented on another page. Take a look at the "Functional updates" section here: https://reactjs.org/docs/hooks-reference.html ("If the new state is computed using the previous state, you can pass a function to setState") – Alberto Trindade Tavares Oct 02 '19 at 07:45
  • @HMR But, to be honest, they could have done a better job in making this more visible. – Alberto Trindade Tavares Oct 02 '19 at 07:47
  • 1
    @AlbertoTrindadeTavares Yes, I was looking at the docs aswell, couldnt find anything. Thanks a lot for the answer! – Bartek Oct 02 '19 at 07:53
  • @Bartek Glad to help :) – Alberto Trindade Tavares Oct 02 '19 at 08:10
  • 1
    Your first solution isn't just an "easy solution", it's the correct method. The second one would only work if the component is designed as a singleton, and even then i'm not sure about that because state becomes a new object each time. – Scimonster Oct 02 '19 at 10:45
  • @Scimonster You're right. It only works if the component is used as a singleton (and it works properly in this case; I have tested). I will remove it from my answer to avoid misleading other people who come to this page. Thanks for the feedback – Alberto Trindade Tavares Oct 02 '19 at 10:56
21

Your functions should be like this:

const changeValue1 = () => {
    setState((prevState) => ({ ...prevState, value1: "new 1" }));
};
const changeValue2 = () => {
    setState((prevState) => ({ ...prevState, value2: "new 2" }));
};

Thus you make sure you are not missing any existing property in the current state by using the previous state when the action is fired. Also thus you avoid to have to manage closures.

Dez
  • 5,702
  • 8
  • 42
  • 51
6

When changeValue2 is invoked, initial state is held so the state turns back to the inital state and then value2 property is written.

Next time changeValue2 is invoked after that, it holds the state {value1: "1", value2: "new 2"}, so value1 property is overwritten.

You need an arrow function for the setState parameter.

const Test = () => {
  const [state, setState] = React.useState({
    value1: "1",
    value2: "2"
  });

  const changeValue1 = () => {
    setState(prev => ({ ...prev, value1: "new 1" }));
  };
  const changeValue2 = () => {
    setState(prev => ({ ...prev, value2: "new 2" }));
  };

  return (
    <React.Fragment>
      <button
        onClick={() => {
          changeValue1();
          setTimeout(changeValue2, 1000);
        }}
      >
        CHANGE BOTH
      </button>
      <h1>{state.value1}</h1>
      <h1>{state.value2}</h1>
    </React.Fragment>
  );
};

ReactDOM.render(<Test />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
zmag
  • 7,825
  • 12
  • 32
  • 42
3

What's happening is that both changeValue1 and changeValue2 see the state from the render they were created in, so when your component render for the first time these 2 functions see:

state= {
  value1: "1",
  value2: "2"
}

When you click on the button, changeValue1 is called first and changes the state to {value1: "new1", value2: "2"} as expected.

Now, after 1 second, changeValue2 is called, but this function still see the initial state ({value1; "1", value2: "2"}), so when this function updates the state this way:

setState({ ...state, value2: "new 2" });

you end up seeing: {value1; "1", value2: "new2"}.

source

Hamza El Aoutar
  • 5,292
  • 2
  • 17
  • 23