2

I have login button and if every input field is filled right (if it is valid) for the first time then it is working normally, but if not, I have to click 3 times to login (first time I get message error that input is invalid, second click clears that error(s) and third loges me in).

import React, { useRef, useContext, useState } from 'react'
import { AppContext } from '../context/AppContext'
import { useHistory } from 'react-router-dom'

function Login() {

    const { setUser } = useContext(AppContext)

    const history = useHistory()

    const usernameRef = useRef<HTMLInputElement>(null)
    const emailRef = useRef<HTMLInputElement>(null)
    const passwordRef = useRef<HTMLInputElement>(null)

    const [valid, setValid] = useState({ usernameErr: '', emailErr: '', passwordErr: '' })

    const handleValidation = () => {
        let usernameMsg = ''
        let passwordMsg = ''
        let emailMsg = ''

        if (usernameRef.current?.value === "")
            usernameMsg = "Username is not valid"
        if (!emailRef.current?.value.includes('@') && !emailRef.current?.value.includes('.'))
            emailMsg = "Email is not valid"
        if (passwordRef.current?.value !== undefined && passwordRef.current.value.length < 5)
            passwordMsg = "Password must contain at least 5 characters"

        setValid({
            usernameErr: usernameMsg,
            emailErr: emailMsg,
            passwordErr: passwordMsg
        })
    }

    const handleLogin = () => {
        handleValidation()
        console.log("validation: ", valid)
        if (usernameRef.current?.value && emailRef.current?.value && passwordRef.current?.value) {
            if (valid.usernameErr === '' && valid.emailErr === '' && valid.passwordErr === '') {
                setUser(
                    {
                        username: usernameRef.current.value,
                        password: passwordRef.current.value,
                        email: emailRef.current.value,
                        isLogin: true
                    })
                history.goBack()
            }
        }
    }

    return (
        <div className="login-outter page">
            <div className="login-inner">
                <p className="login-title">Log in here</p>
                <input type="text" placeholder="Username" ref={usernameRef} className="about-input"/>
                <p className="valid">
                    {valid.usernameErr}
                </p>
                <input type="text" placeholder="Email" ref={emailRef} className="about-input"/>
                <p className="valid">
                    {valid.emailErr}
                </p>
                <input type="password" placeholder="Password" ref={passwordRef} className="about-input"/>
                <p className="valid">
                    {valid.passwordErr}
                </p>
                <button className="login-btn" onClick={handleLogin}>Login</button>
            </div>
        </div>
    )
}

export default Login

Here is code sandbox: https://codesandbox.io/s/flamboyant-torvalds-tevby?file=/src/components/pages/Login.tsx

I assume it is because of the useState, but don't know what exactly and why and how to fix this.

1
  • React state hook updates are asynchronous so you cannot guarantee that the state will be updated between the time that you call handleValidation() and when you actually are checking the state. Commented May 4, 2020 at 20:37

2 Answers 2

1

This is because React state hook updates are asynchronous as I mentioned in the comments. Here is an example of how you could fix this by slightly modifying your current code: (Not the most elegant solution but its the smallest changes to your code and it works)

    const handleValidation = () => {
        let usernameMsg = ''
        let passwordMsg = ''
        let emailMsg = ''

        if (usernameRef.current?.value === "")
            usernameMsg = "Username is not valid"
        if (!emailRef.current?.value.includes('@') && !emailRef.current?.value.includes('.'))
            emailMsg = "Email is not valid"
        if (passwordRef.current?.value !== undefined && passwordRef.current.value.length < 5)
            passwordMsg = "Password must contain at least 5 characters"

        setValid({
            usernameErr: usernameMsg,
            emailErr: emailMsg,
            passwordErr: passwordMsg
        })
        return (usernameMsg === '' && passwordMsg === '' && emailMsg === '')
    }

    const handleLogin = () => {
        if (handleValidation() && usernameRef.current?.value && emailRef.current?.value && passwordRef.current?.value) {
                setUser(
                    {
                        username: usernameRef.current.value,
                        password: passwordRef.current.value,
                        email: emailRef.current.value,
                        isLogin: true
                    })
                history.goBack()
        }
    }

Here it is at your link: https://codesandbox.io/s/strange-lamport-olcy0?file=/src/components/pages/Login.tsx

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks! Can you explain me why returning this (usernameMsg === '' && passwordMsg === '' && emailMsg === '') ?
That will return true if there were no errors in your handleValidation() function. It is returning the results of the handleValidation() function without depending on state since the state update is asynchronous. This way you can just see if handleValidation() returns true and this replaces this line that you had previously: if (valid.usernameErr === '' && valid.emailErr === '' && valid.passwordErr === '')
1

handleLogin() gets the valid state value from the render cycle that the function was executed (button was clicked).

That means when you call handleValidation() within handleLogin() it will not affect the valid state in the same context immediately, but will affect the state in the next render cycle where a new handleLogin function will be created (and won't be executed until the button is clicked).

The solution is to have a validator function like handleValidation() return a validation result directly, then you can set error messages state inside handleLogin()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.