useState hook setter incorrectly overwrites state

Viewed 9788

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;
4 Answers

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.

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.

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>

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

Related