0

I am currently messing around with client-side form validation. There is no back end or anything along those lines. I have an on submit function, and I am trying to push the user to the home page once a valid form has been submitted using props.history.push. For some reason, I have to submit the form twice for the on submit function to actually push the user into the home page. I am unsure of why this is happening. Below I am going to provide my useForm hook, my Login page, and my validation function I am using. I feel like the answer might be obvious, but I just can't find the issue.

useForm Hook:

import { useState } from "react"

const INITIAL_STATE = {
  email: "",
  password: ""
}

const UseForm = (ValidateLogin, props) => {
  const [formData, setFormData] = useState(INITIAL_STATE)
  const [errors, setErrors] = useState({})
  const [user, setUser] = useState(null)

  const handleChange = field => e => {
    setFormData({ ...formData, [field]: e.target.value })
  }

  const handleSubmit = event => {
    event.preventDefault()
    setUser(formData)
    setErrors(ValidateLogin(formData))
    user && !errors.email && !errors.password && props.history.push("/")
  }
  return {
    handleChange,
    handleSubmit,
    formData,
    user,
    errors
  }
}

export default UseForm

Login Page:

import React from "react"
import UseForm from "../Login/UseForm"
import ValidateLogin from "../Login/ValidateLogin"
import { makeStyles } from "@material-ui/core/styles"

// Material UI
import TextField from "@material-ui/core/TextField"
import Button from "@material-ui/core/Button"
import Typography from "@material-ui/core/Typography"
import Container from "@material-ui/core/Container"

const useStyles = makeStyles(theme => ({
  form: {
    textAlign: "center",
    width: "100%", // Fix IE 11 issue.
    marginTop: theme.spacing(1),
    position: "relative"
  },
  logo: {
    width: 80,
    margin: "20px auto 20px auto"
  },
  paper: {
    marginTop: theme.spacing(8),
    display: "flex",
    flexDirection: "column",
    alignItems: "center"
  },
  submit: {
    margin: theme.spacing(3, 0, 2),
    position: "relative"
  },
  progress: {
    position: "absolute"
  },
  customError: {
    color: "red",
    fontSize: "0.8rem",
    width: "100%",
    position: "absolute"
  }
}))

const Login = props => {
  const classes = useStyles()

  const { handleChange, handleSubmit, formData, user, errors } = UseForm(
    ValidateLogin,
    props
  )

  const isInvalid = !formData.email || !formData.password

  return (
    <div style={{ textAlign: "center" }}>
      <Typography variant='h5' gutterBottom>
        Welcome, Please Login
      </Typography>
      <Container component='main' maxWidth='xs'>
        <form className={classes.form} onSubmit={handleSubmit}>
          <TextField
            variant='outlined'
            fullWidth
            margin='normal'
            label='Email'
            type='text'
            name='email'
            value={formData.email}
            error={errors.email ? true : false}
            helperText={errors.email}
            onChange={handleChange("email")}
          />
          <TextField
            variant='outlined'
            margin='normal'
            fullWidth
            label='Password'
            type='password'
            name='password'
            value={formData.password}
            error={errors.password ? true : false}
            helperText={errors.password}
            onChange={handleChange("password")}
          />
          <br />
          <Button
            variant='outlined'
            color='primary'
            type='submit'
            disabled={isInvalid}
            className={classes.submit}
          >
            Submit
          </Button>
        </form>
      </Container>
      <br />
      {user &&
        !errors.email &&
        !errors.password &&
        JSON.stringify(user, null, 2)}
    </div>
  )
}

export default Login

Validate Login:

export default function ValidateLogin(formData) {
  let errors = {}

  if (!formData.email) {
    errors.email = "Email address is required"
  } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
    errors.email = "Email address is invalid"
  }
  if (!formData.password) {
    errors.password = "Password is required"
  } else if (formData.password.length < 6) {
    errors.password = "Password must be longer than 6 characters"
  }

  return errors
}

Any helpful suggestions or feedback would be greatly appreciated!! I apologize for the long-winded question. Thanks for taking the time to read this.

Rob Terrell
  • 2,398
  • 1
  • 14
  • 36

1 Answers1

1

It looks like your issue is the conditional in this block:

setUser(formData)
setErrors(ValidateLogin(formData))
user && !errors.email && !errors.password && props.history.push("/")

user and errors will not be updated by the time this condition is evaluated because state setters are async. Here is a more detailed explanation.

This is why it works on the second submit. The values have then been updated, and the condition passes.

This may not be the desired end solution, but it should solve the problem for you.

const handleSubmit = event => {
  event.preventDefault()
  setUser(formData)

  // Use temp variable so that you can use it immediately below
  const tempErrors = ValidateLogin(formData);
  setErrors(tempErrors)

  // Use the formData directly and the tempErrors
  formData && !tempErrors.email && !tempErrors.password && props.history.push("/")
}
Brian Thompson
  • 13,263
  • 4
  • 23
  • 43
  • this definitely works, is there a way to prevent having to submit twice without having to set up the temp errors conditional – Rob Terrell Mar 13 '20 at 17:40
  • 1
    I'm pretty sure it would be acceptable to put a `useEffect` inside your custom hook that listens for changes to `errors` and `user`, and have it change the page. – Brian Thompson Mar 13 '20 at 18:12
  • 1
    But really, as is this doesn't make for a very flexible custom hook. I would make the hook accept another parameter just like it does for the validation logic (maybe called submit). And then call `submit` instead of change the page. That way you can reuse this hook with a few more tweaks. – Brian Thompson Mar 13 '20 at 18:15