useState updated state not available in the same handler even with timeout

I have a simple registration form with 3 fields. I have stored the state in formValues with value & error associated with each field. Now when i submit the form without filling any or at least one field the form should be invalid but instead it shows validation messages with invalid fields but makes form valid. Even if i have added setTimeout the updated state is not available in the same handleSubmit. If i submit again the process works just fine. I understand that the state updation is async but if we see the logs in console the form’s validation message is logged after formValues log in the render and those logs show that the state was updated correctly but the final validation message shows invalid state. If i change it to class component it works. Here’s a link to codesandbox.

import React, { useState } from "react";
import { Button, Form, Col } from "react-bootstrap";

const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout));

const RegistrationForm = () => {
  const [formValues, setFormValues] = useState({
    name: { value: "", error: null },
    email: { value: "", error: null },
    password: { value: "", error: null }
  });

  const handleInputChange = (e, field) => {
    const { value } = e.target;
    setFormValues(prevValues => ({
      ...prevValues,
      [field]: { value, error: null }
    }));
  };

  const validateForm = () => {
    let updatedFormValues = { ...formValues };

    Object.keys(formValues).forEach(field => {
      if (!formValues[field].value) {
        updatedFormValues = {
          ...updatedFormValues,
          [field]: { ...updatedFormValues[field], error: "required" }
        };
      }
    });

    setFormValues(updatedFormValues);
  };

  const isFormValid = () =>
    Object.keys(formValues).every(field => formValues[field].error === null);

  const handleSubmit = async e => {
    e.preventDefault();

    validateForm();

    await sleep(100);

    if (!isFormValid()) {
      console.log("form is not valid", formValues);
      return;
    }

    console.log("form is valid", formValues);

    // make api call to complete registration
  };

  console.log({ formValues });

  return (
    <Form className="registration-form" onSubmit={handleSubmit}>
      <Form.Row>
        <Col>
          <Form.Group controlId="name">
            <Form.Label>Name</Form.Label>
            <Form.Control
              type="text"
              placeholder="Enter name"
              value={formValues.name.value}
              onChange={e => handleInputChange(e, "name")}
            />
            <Form.Control.Feedback type="invalid" className="d-block">
              {formValues.name.error}
            </Form.Control.Feedback>
          </Form.Group>
        </Col>
        <Col>
          <Form.Group controlId="email">
            <Form.Label>Email</Form.Label>
            <Form.Control
              type="email"
              placeholder="Enter email"
              value={formValues.email.value}
              onChange={e => handleInputChange(e, "email")}
            />
            <Form.Control.Feedback type="invalid" className="d-block">
              {formValues.email.error}
            </Form.Control.Feedback>
          </Form.Group>
        </Col>
      </Form.Row>
      <Form.Row>
        <Col>
          <Form.Group controlId="password">
            <Form.Label>Password</Form.Label>
            <Form.Control
              type="password"
              placeholder="Enter password"
              value={formValues.password.value}
              onChange={e => handleInputChange(e, "password")}
            />
            <Form.Control.Feedback type="invalid" className="d-block">
              {formValues.password.error}
            </Form.Control.Feedback>
          </Form.Group>
        </Col>
        <Col />
      </Form.Row>
      <Button variant="primary" type="submit">
        Submit
      </Button>
    </Form>
  );
};

export default RegistrationForm;

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

State updates are not just async but are als affected by closures in functional components, so using a sleep or timeout isn’t going to leave your with an updated value in the same render cycle

You can read more about it in this post:

useState set method not reflecting change immediately

However, one solution in your case is to maintain a ref and toggle is value to trigger a useEffect in which you will validate the form post handleSubmit handler validates it and sets the formValues

Relevant code:

const validateFormField = useRef(false);

  const handleInputChange = (e, field) => {
    const { value } = e.target;
    setFormValues(prevValues => ({
      ...prevValues,
      [field]: { value, error: null }
    }));
  };

  const validateForm = () => {
    let updatedFormValues = { ...formValues };

    Object.keys(formValues).forEach(field => {
      if (!formValues[field].value) {
        updatedFormValues = {
          ...updatedFormValues,
          [field]: { ...updatedFormValues[field], error: "required" }
        };
      }
    });

    setFormValues(updatedFormValues);
    validateFormField.current = !validateFormField.current;
  };

  const isFormValid = () =>
    Object.keys(formValues).every(field => formValues[field].error === null);

  const handleSubmit = async e => {
    e.preventDefault();

    validateForm();

    // make api call to complete registratin
  };

  useEffect(() => {
    if (!isFormValid()) {
      console.log("form is not valid", formValues);
    } else {
      console.log("form is valid", formValues);
    }
  }, [validateFormField.current]); // This is fine since we know setFormValues will trigger a re-render

Working demo

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply