skip to Main Content

I am writting a simple form for registring an account. The user should input his name, e-mail, password and a confirmation of his password.

My validates the password inside the onChange event in the input html. Here’s my component:

import React, { useState } from 'react'
import Input from './Input'

const Register = () => {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [pswdConfirm, setPswdConfirm] = useState('')

  function validatePassword() {
    setPasswordError('')
    setPswdConfirmError('')
    
    if (password.length < 6) {
      setPasswordError('Password must be at least 7 characters longs')
      return false
    }

    if (password !== pswdConfirm) {
      setPswdConfirmError('Passwords are not identical')
      return false
    }

    return true
  }

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

    if (!validatePassword()) { // here it works
      return;
    }

  }

  return (
    <div className='d-flex justify-content-center align-items-center fullscreen'>
      <form className='h-50 d-block central-card container'>
        <Input 
          label='Fullname'
          type='text'
          onChange={(e) => setName(e.target.value)}
        />
        <Input 
          label='E-mail'
          type='text'
          onChange={(e) => setEmail(e.target.value)}
        />
        <Input 
          label='Password'
          type='password'
          error={passwordError}
          onChange={(e) => {
            setPassword(e.target.value)
            validatePassword() // here it doesnt
            console.log('password', password)
          }}
        />
        <Input 
          label='Confirm password'
          type='password'
          error={pswdConfirmError}
          onChange={(e) => {
            setPswdConfirm(e.target.value)
            validatePassword() // and neither here
            console.log('password confirmation', pswdConfirm)
          }}
        />

        <button className="w-100 row align-items-center" onClick={(e) => onSubmit(e)}>
          <span className="text-center">Sign-up</span>
        </button>
      </form>
    </div>
  )
}

export default Register

However, my setter from react hooks (‘setPassword’ and ‘setPswdConfirm’) do not seem to update my component state asynchronously which results in my ‘onChange’ valiation constantly failing, as the actual value in my component is always lagging one char behind the actual input. For example, when I type ‘1’, my function will try to validate ”, when I type ’12’ it will instead execute with ‘1’, when I input ‘123’, ’12’ and so on.

I was expecting my react-hook setter to syncrhously update my state so I could promptly valite the password. Am I missing something or not using this library appropriately?

I could instead run my valiation on e.target.value, however I’d like to understand react-hooks better and try for a more elegant solution before trying workarounds.Should I instead use ‘useEffect’?

2

Answers


  1. I think you are better off using useEffect to synchronise your validation.
    From react docs:

    setState() does not immediately mutate this.state but creates a
    pending state transition. Accessing this.state after calling this
    method can potentially return the existing value.

    There is no guarantee of synchronous operation of calls to setState
    and calls may be batched for performance gains.

    useEffect version:

    
    import React, { useState, useEffect } from 'react'
    
    const Input = (props) => <input {...props} />;
    
    const Register = () => {
      const [name, setName] = useState('')
      const [email, setEmail] = useState('')
      const [password, setPassword] = useState('')
      const [passwordError, setPasswordError] = useState('')
      const [pswdConfirm, setPswdConfirm] = useState('')
      const [pswdConfirmError, setPswdConfirmError] = useState('')
    
      const validatePassword = () => {
        setPasswordError('')
        setPswdConfirmError('')
        
        if (password.length < 6) {
          setPasswordError('Password must be at least 7 characters longs')
          return false
        }
    
        if (password !== pswdConfirm) {
          setPswdConfirmError('Passwords are not identical')
          return false
        }
    
        return true
      };
    
      useEffect(() => {
        validatePassword();
      }, [password, pswdConfirm]);
    
      const onSubmit = async (e) => {
        e.preventDefault();
    
        if (!validatePassword()) { // here it works
          return;
        }
    
      }
    
      return (
        <div className='d-flex justify-content-center align-items-center fullscreen'>
          <form className='h-50 d-block central-card container'>
            <Input 
              label='Fullname'
              type='text'
              onChange={(e) => setName(e.target.value)}
            />
            <Input 
              label='E-mail'
              type='text'
              onChange={(e) => setEmail(e.target.value)}
            />
            <Input 
              label='Password'
              type='password'
              error={passwordError}
              onChange={(e) => {
                setPassword(e.target.value)
                validatePassword() // here it doesnt
                console.log('password', password)
              }}
            />
            <p style={{ color: 'red' }}>{passwordError}</p>
            <Input 
              label='Confirm password'
              type='password'
              error={pswdConfirmError}
              onChange={(e) => {
                setPswdConfirm(e.target.value)
                validatePassword() // and neither here
                console.log('password confirmation', pswdConfirm)
              }}
            />
            <p style={{ color: 'red' }}>{pswdConfirmError}</p>
    
            <button className="w-100 row align-items-center" onClick={(e) => onSubmit(e)}>
              <span className="text-center">Sign-up</span>
            </button>
          </form>
        </div>
      )
    }
    
    export default Register
    
    Login or Signup to reply.
  2. You have to use the useEffect hook in order for the validation to be real time, the useEffect runs everytime the dependency changes (the dependency is the second argument which is an array), so I will modify your code like this and remove the validatePassword fn.

    export default function App() {
      const [name, setName] = useState("");
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
      const [pswdConfirm, setPswdConfirm] = useState("");
      const [passwordError, setPasswordError] = useState("");
      const [pswdConfirmError, setPswdConfirmError] = useState("");
    
      useEffect(() => {
        if (password.length < 6) {
          setPasswordError("Password must be at least 7 characters longs");
        } else {
          setPasswordError("");
        }
    
        if (password !== pswdConfirm) {
          setPswdConfirmError("Passwords are not identical");
        } else {
          setPswdConfirmError("");
        }
      }, [password, pswdConfirm]);
    
      const onSubmit = async (e) => {
        e.preventDefault();
    
        if (passwordError || pswdConfirmError) {
          return;
        }
      };
    
      return (
        <div className="d-flex justify-content-center align-items-center fullscreen">
          <form className="h-50 d-block central-card container">
            <input
              // label='Fullname'
              type="text"
              name={name}
              onChange={(e) => setName(e.target.value)}
            />
            <input
              // label='E-mail'
              type="text"
              name={email}
              onChange={(e) => setEmail(e.target.value)}
            />
            <input
              // label='Password'
              type="password"
              name={password}
              error={passwordError}
              onChange={(e) => {
                setPassword(e.target.value);
                console.log("password", password);
              }}
            />
            <input
              // label='Confirm password'
              type="password"
              name={pswdConfirm}
              onChange={(e) => {
                setPswdConfirm(e.target.value);
              }}
            />
            {passwordError && <p>{passwordError}</p>}
            {pswdConfirmError && <p>{pswdConfirmError}</p>}
            <button
              className="w-100 row align-items-center"
              onClick={(e) => onSubmit(e)}
            >
              <span className="text-center">Sign-up</span>
            </button>
          </form>
        </div>
      );
    }
    

    Explanation:
    Every time the password or pswdConfirm changes the useEffect will run whatever is inside the function because they are included in the dependency list [password, pswdConfirm]. Aditionally I added the reset errors in an else statement, so you don’t call it everytime the input changes.

    While your validate password returns a boolean, this is not actually needed, because you can validate yourself using the passwordError and pswdConfirmError values in the onSubmit function like in the code I shared

      const onSubmit = async (e) => {
        e.preventDefault();
    
        if (passwordError  || pswdConfirmError) {
          return;
        }
        // Do the magic here
      }
    

    The validatePassword will work but I think is redundant, so what you want to do in your effect is to modify the passwordError and pswdConfirmError. Please modify the code accordingly because I used regular inputs instead of the Input component you shared and remember to add the name prop to the inputs to match with the labels and improve a11y.

    Here’s the code sandbox in case you want to look how it looks:
    https://codesandbox.io/s/amazing-stallman-yd6g9h?file=/src/App.js

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search