0

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;

Kamran Arshad
  • 81
  • 1
  • 16

1 Answers1

0

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

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Thank you for the in depth explanation link & answer. However there's a small issue with your code that when the component mounts first it makes the form valid so this should be fixed by adding something like `isSubmitting` in `useEffect`. – Kamran Arshad Jun 08 '20 at 15:12