bami
bami

Reputation: 299

Why can't I have updated states in function component (using hook)?

I have login function component with two inputs. It's controlled component so email and password are bound to state(user). Input is a component I use instead of input tag(refactoring input). I can change state user(email and password) with input values using handleInputChange event handler and I also can submit form using handleSubmit handler.

Everything was good until I tried to validate form using yup and catching errors. I declared errors state to save errors I got. I want to catch errors and show in "div className="alert"" and I want to post user to server when no error exists. I see the errors related to yup in validate() function, but when I change errors state(setErrors([...er.errors])) I find errors state empty (console.log(errors.length)).

Here is login component:

import axios from "axios";
import queryString from "query-string"
import { useEffect, useRef,useState } from "react";
import React from "react"
import {useLocation, useRouteMatch,useParams} from "react-router-dom"
import Input from "./input";
import * as yup from 'yup';
const Login = () => {
    useEffect(async()=>{
   
   
       
        console.log(errors)
    },[errors])

    var [user,setUser]=useState({email:'',password:''});
 var [errors,setErrors]=useState([])
 let schema=yup.object().shape({
    email:yup.string().email("ایمیل نامعتبر است").required("فیلد ایمیل الزامیست"),
    password:yup.string().min(8,"رمز عبور باید حداقل 8 رقم باشد")
})
const validate=async()=>{
    try  {
       const resultValidate=await schema.validate(user, { abortEarly: false })
       
    }
    catch(er){
       console.log(er.errors)
      setErrors([...er.errors])
     
       
    }
   }
  const  handleSubmit= async(e)=>{
      
e.preventDefault();
await validate();
console.log(errors.length)
if(errors.length===0){
    alert("X")
   const response= await axios.post("https://reqres.in/api/login",user)
   console.log(response)
 }
  }

const handleInputChange=async(e)=>{
 
  setUser({...user,[e.currentTarget.name]:e.currentTarget.value})


}
    return ( 
    <>
     <div id="login-box" className="col-md-12">
                        
            
                        {errors.length!==0  && (<div className="alert">
                            <ul>
                               {errors.map((element,item)=>{
                                    return(
                                    <>
                                     <li key={item}>
                                           {element}
                                    </li>
                                    </>
                                   )
                                })}
                            </ul>
                        </div>) }
                        <form onSubmit={handleSubmit} id="login-form" className="form" action="" method="post">
                            <h3 className="text-center text-info">Login</h3>
                            <Input  onChange={handleInputChange} name="email" id="email" label="نام کاربری" value={user.email}/>
                            <Input  name="password" onChange={handleInputChange} id="password" value={user.password} label="رمز عبور"/>
                            
                         
                          
                            {/* <div id="register-link" className="text-right">
                                <a href="#" className="text-info">Register here</a>
                            </div> */}
                            <input type="submit"   className="btn btn-primary" value="ثبت"/>
                        </form>
                    </div>
    </>
    );
}
 
export default Login;

and here is Input component:

import {Component} from "react"
class Input extends Component {
    render() { 
        return <>
        
   <div className="form-group">
   <label htmlFor="username" className="text-info">{this.props.label}</label><br/>
                                <input type="text" onChange={this.props.onChange} name={this.props.name} id={this.props.id} className="form-control" value={this.props.value} />
   </div>

        </>;
    }
}
 
export default Input;

I understood that setStates(in my component setErrors) are asynchronous and it's delayed. I tried using simple array variable (named errors) instead of state and hook, but guess what, it didn't rerender page when I changed the errors variable! Of course I can't see errors in page using this way.

I tried to resolve this using useEffect() and I decided to check validation errors and post in useEffect instead of handleSubmit handler:

 useEffect(async()=>{
   
    if(errors.length===0){

        const response= await axios.post("https://reqres.in/api/login",user)
        console.log(response)
      }
   
    console.log(errors)
}, [errors])

Now I see errors when inputs are invalid. When I type valid values, there are still same errors! It looks like I can't have updated errors state and I just get previous errors even after I enter valid values! I try to not use class based component as I can. What shall I do?

Upvotes: 1

Views: 268

Answers (2)

Drew Reese
Drew Reese

Reputation: 202618

Issue

The issue you face is that React state updates are asynchronously processed. This doesn't mean that the state update is async and can be waited for. The errors state you enqueue won't be available until the next render cycle.

const validate = async () => {
  try  {
    const resultValidate = await schema.validate(user, { abortEarly: false });       
  } catch(er) {
    console.log(er.errors);
    setErrors([...er.errors]); // (2) <-- state update enqueued
  }
}

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

  await validate(); // (1) <-- validate called and awaited

  console.log(errors.length); // <-- (3) errors state from current render cycle

  if (errors.length === 0) {
    alert("X");
    const response = await axios.post("https://reqres.in/api/login", user);
    console.log(response);
  }
}

Solution

I suggest returning an "errors" object from validate instead, you can enqueue any state updates later if you like.

const validate = async () => {
  const errors = [];
  try  {
    await schema.validate(user, { abortEarly: false });
  } catch(er) {
    console.log(er.errors);
    errors.push(...er.errors);
  }
  return errors;
}

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

  const errors = await validate();

  console.log(errors.length);

  if (!errors.length) {
    alert("X");
    const response = await axios.post("https://reqres.in/api/login", user);
    console.log(response);
  } else {
    setErrors(prevErrors => [...prevErrors, ...errors]);
  }
}

Upvotes: 1

Pariskrit Moktan
Pariskrit Moktan

Reputation: 111

You can return true if the input values are validated and false if not, from the validate function like this:

const validate = async () => {
try {
  const resultValidate = await schema.validate(user, { abortEarly: false });
  return true;
} catch (er) {
  console.log(er.errors);
  setErrors([...er.errors]);
  return false;
}
};

And now in the handleSubmit function you have to modify a bit:

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

 console.log(errors.length);

 if (isValid) {
  alert("X");
  const response= await axios.post("https://reqres.in/api/login",user)
  console.log(response)
  setErrors([]);  //so that the previous errors are removed
 }
 };

Upvotes: 1

Related Questions