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

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">
Register here
</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?

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
}
};

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]);
}
}

Related

React hook form returning blank object on submit

Here's my simplified problem:
https://codesandbox.io/s/busy-fire-mm91r?file=/src/FormWrapper.tsx
And the code:
export const FormItemWrapper = ({ children }) => {
return <FormItem>{children}</FormItem>;
};
export const FormWrapper = ({ children }) => {
const { handleSubmit } = useForm();
const onSubmit = (data) => {
console.log(data); // logs { }
};
return <Form onFinish={handleSubmit(onSubmit)}>{children}</Form>;
};
export default function App() {
const { control, watch } = useForm();
console.log(watch()); // logs { }
return (
<FormWrapper>
<FormItemWrapper>
<Controller
control={control}
name="tmp"
render={({ field }) => <Input {...field} />}
/>
</FormItemWrapper>
<FormItemWrapper>
<Button htmlType="submit">Save</Button>
</FormItemWrapper>
</FormWrapper>
);
}
The problem:
React-hook-form doesn't seem to see the data I type in. I can get it using antd, but can't with react-hook-form. Why? What am I missing?
watch() logs only once, and it logs { }. onSubmit logs { }
You have created two different form instances with useForm call. If you want to get current form context inside Controller you should use useFormContext and wrap your form in FormProvider.
Working example:
https://codesandbox.io/s/admiring-lehmann-mgd0i?file=/src/App.tsx

How to get useState values inside getServerSideProps in NextJS

I have two functions one function takes input from user and other performs search for that supplied input data. I want to use searchQuery from useState inside getServerSideProps function.
Is there any way to do this?
function SearchDemo() {
const [searchQuery, setSearchQuery] = useState('')
const ChangeHandler = event => {
setSearchQuery(event.target.value)
}
const ButtonHandler = event => {
event.preventDefault()
console.log(`Data: ${searchQuery}`)
}
return (
<div>
<form onSubmit={ButtonHandler}>
<div>
<label>Search:</label>
<input type="text" name="search" value={searchQuery} onChange={ChangeHandler}/>
<button>Search</button>
</div>
</form>
</div>
);
}
export async function getServerSideProps({query: {page}, searchQuery}){
console.log(searchQuery)
return {
props: {
data: data
}
}
}
You need to push the url and reload the page, because it's serverside-rendered.
const router = useRouter()
const ButtonHandler = event => {
event.preventDefault()
router.push(`/thecurrentUrl?search=${searchQuery}`
// if doesn't work then use window.location.href
console.log(`Data: ${searchQuery}`)
}
export async function getServerSideProps({query: {page}, searchQuery}){
console.log(query.search)
return {
props: {
data: data
}
}
}

"event" in Javascript is deprecated and I cannot use "preventDefault()"

I am trying to use the event.preventDefault() method but I am continuously receiving error. It says that event has been deprecated.
I am making a Firebase SignUp form and I want to prevent the form from Submitting.
Here is the complete code.
import React from "react"
import styled from "styled-components"
import getFirebase from "../../firebase"
import useInput from "./useInput"
const SignUpForm = () => {
const firebaseInstance = getFirebase()
const email = useInput("")
const password = useInput("")
const signUp = async () => {
event.preventDefault()
try {
if (firebaseInstance) {
const user = await firebaseInstance
.auth()
.createUserWithEmailAndPassword(email.value, password.value)
console.log("user", user)
alert(`Welcome ${email.value}!`)
}
} catch (error) {
console.log("error", error)
alert(error.message)
}
}
event.preventDefault()
return (
<FormWrapper onSubmit={() => signUp()}>
<Title>Sign up</Title>
<Input placeholder="Email" {...email} />
<Input placeholder="Password" type="password" {...password} />
<Button type="submit">Sign up</Button>
</FormWrapper>
)
}
export default SignUpForm
And the useInput code:
import { useState } from "react"
const useInput = initialValue => {
const [value, setValue] = useState(initialValue)
const handleChange = event => {
setValue(event.target.value)
}
return {
value,
onChange: handleChange,
}
}
export default useInput
What that warning means is that the global variable window.event is deprecated. You can still access the event associated with an event handler, you just have to go about it the proper way - by using the parameter from the handler callback.
Change
<FormWrapper onSubmit={() => signUp()}>
to
<FormWrapper onSubmit={signUp}>
and then signUp's first parameter will be the event, and you'll be able to use it and call preventDefault on it as you're trying.
const signUp = async (event) => {
But don't put event.preventDefault() in your functional component's main body - that is, it shouldn't be here:
event.preventDefault()
return (
...
Only put it inside the signUp handler.

How to input an existing value into an input field in React for an edit form

I have an existing Task with a title and a description, and I want to navigate to an edit form. By default I want the existing title and description values to populate the inputs using React. The important piece of this code I'm asking about is value={task.title}. Please ignore how the data is being pulled in (I'm new to React and I'm experimenting). The onChange and onSubmit handles work correctly, but the error obviously indicates I'm doing it wrong and it does cause occasional bugs.
I've tried abstracting those values into some sort of formValues state as well, but no matter how the values are being input, if the value={howeverIDoIt} is being directly manipulated I get the error.
import React, { useEffect, useState } from 'react';
import { HEADERS, TODO_URL } from '../urls';
import { useHistory, useParams } from 'react-router-dom';
const TaskEdit = () => {
const [task, setTask] = useState({});
const { id } = useParams();
const history = useHistory();
useEffect(() => {
fetch(`${TODO_URL}/api/tasks/${id}/`, {headers: HEADERS})
.then(response => response.json())
.then(responseJson => {
setTask(responseJson);
});
}, []);
const handleChange = (e) => {
setTask(e.target.value)
}
const handleSubmit = (e) => {
e.preventDefault();
const body = {
'title': e.target.form[0].value,
'description': e.target.form[1].value
}
fetch(
`${TODO_URL}/api/tasks/${id}/edit/`,
{
headers: HEADERS,
method: 'PUT',
body: JSON.stringify(body)
}
).then(res => res).catch(err => err);
history.push(`/task/${id}`)
}
return (
<form>
<div>
<label>Title</label>
<input type="text" onChange={handleChange} value={task.title} />
</div>
<div>
<label>Description</label>
<textarea onChange={handleChange} value={task.description}></textarea>
</div>
<button onClick={handleSubmit}>Submit</button>
</form>
);
}
export default TaskEdit;
I have tried putting in a default value for useState like so: useState({title: 'title', description: 'description'}) but that doesn't prevent the error, nor does adding this edit form into the Task.js component, where task is most definitely defined.
You have:
<input type="text" onChange={handleChange} value={task.title} />
Your handleChange method is:
const handleChange = (e) => {
setTask(e.target.value)
}
When your onChange fires, your task state will be set to a String (the value of <input />)
So when you are referencing task.title after your onChange fires, it will be undefined. The same is true for task.description.
Try this:
const handleTitleChange = (e) => {
setTask({...task, title: e.target.value})
}
const handleDescriptionChange = (e) => {
setTask({...task, description: e.target.value})
}
<input type="text" onChange={handleTitleChange} value={task.title} />
<textarea onChange={handleDescriptionChange} value={task.description} />
Alternatively, you could split up the task state to title and description, respectively.

const { data } = useQuery problem: I want data to be object initially

I'm trying to implement a login function to my react app.
import React, { useState, useEffect } from 'react'
import { useQuery, useLazyQuery, useMutation } from "#apollo/client"
import { useForm } from "react-hook-form"
import { LOGIN } from '../queries/queries'
const Login = () => {
const [formValue, setFormValue] = useState({})
const { loading, error, data } = useQuery(LOGIN, {
variables: {
email: formValue.email,
password: formValue.password
}
})
const { register, handleSubmit } = useForm()
const onSubmit = (value) => {
setFormValue(value)
}
if (loading) return <p>loading</p>
return(
<>
<form onSubmit={handleSubmit(onSubmit)} >
<input
type="text"
name="email"
placeholder="E-mail"
ref={register}
/>
<input
type="password"
name="password"
placeholder="Password"
ref={register}
/>
<button type="submit">
Login
</button>
</form>
</>
)
}
When I code console.log(data.user) for example, error happens because user is not undefined.
I know I can get object from data if I code variables directly, but I want to get it after handleSubmit.
I think if I can make data object initially, error would not happen.
Then is there any way to do that?
try "data?.user" instead of "data.user" when referring to that object attribute
the question mark should disable the error if the object doesnt exist
update:
you can also try declaring data as an empty objec literal:
{ loading, error, data = {} }

Categories

Resources