Ravi
Ravi

Reputation: 3756

React useState hook - when to use previous state when updating state?

In the code below, the form is not updated correctly unless I pass in previous state to the state setting call ('setForm').

To see it in action, run the code as-is, and the console will print 'true', indicating it worked. That's using 'validate1' function. If you replace that with 'validate2' (which doesn't use previous state), it fails to print 'true'.

It seems like in validate2, the second setForm call overwrites the form.long state of the first setForm call, due to the async nature of these calls. But why does validate1 work? Why does using previous state cause it to work? Any documentation on this behavior would be really helpful.

(I can make validate2 work by having one setForm that sets both fields, but the code below is contrived to show a difference in the two ways setForm can be called, with and without previous state.)

CodeSandbox

const {useState, useEffect} = React;


function LoginForm() {
  const [form, setForm] = useState({
    username: "",
    password: "",
    long: null
  });

  useEffect(() => {
    console.log(form.long);
  }, [form.long]);

  const validate1 = (e) => {
    e.preventDefault();
    setForm((prevState) => ({
      ...prevState,
      long: form.password.length >=3 ? true : false
    }));
    setForm((prevState) => ({
      ...prevState,
      username: "*****"
    }));
  };

  const validate2 = (e) => {
    e.preventDefault();
    setForm({
      ...form,
      long: form.password.length >=3 ? true : false
    });
    setForm({
      ...form,
      username: "*****"
    });
  };

  const updateField = (e) => {
    setForm({
      ...form,
      [e.target.name]: e.target.value
    });
  };

  return (
    <form onSubmit={validate1}>
      <label>
        Username:
        <input value={form.username} name="username" onChange={updateField} />
      </label>
      <br />
      <label>
        Password:
        <input
          value={form.password}
          name="password"
          type="password"
          onChange={updateField}
        />
      </label>
      <br />
      <button>Submit</button>
    </form>
  );
}


// Render it
ReactDOM.render(
  <LoginForm/>,
  document.getElementById("react")
);
<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="react"></div>

Upvotes: 1

Views: 9276

Answers (2)

Ravi
Ravi

Reputation: 3756

In the React documentation, they mention the following,

If the next state depends on the current state, we recommend using the updater function form, instead:

this.setState((state) => {
  return {quantity: state.quantity + 1};
});

That's the reason validate1 works, because the second call to setForm depends on the previous setForm call state, and the state will be up to date if the updater function form is used.

Upvotes: 5

Raj
Raj

Reputation: 22926

Why does validate1 work though? Why does using previous state cause it to work?

setForm({
  ...form,
  long: form.password.length >=3 ? true : false
});
setForm({
  ...form,
  username: "*****"
});

The value of form in the second setForm is still the old value until the next re-render. It doesn't reflect the updated form from the previous setForm.

React setState and useState does not make changes directly to the state object.

setState and useState create queues for React core to update the state object of a React component.

So the process to update React state is asynchronous for performance reasons. That’s why changes don’t feel immediate.

https://linguinecode.com/post/why-react-setstate-usestate-does-not-update-immediately

Upvotes: 1

Related Questions