This question already has answers here:
How to use `setState` callback on react hooks
(22 answers)
Closed 2 years ago.
There are 2 event handlers that handle passwords.
The first inputPassword writes the value from its input to state
the second inputPasswordRE writes the value of input and compares it to the value from inputPassword.
Since setState works asynchronously, even if the same values are entered, the check fails, so
how in inputPasswordRE its previous state is compared (When you enter in the password field - 12345 and in the re_password field - 12345, their values password === re_password will be false.).
How to correctly write setState in inputPasswordRE so that
did the comparison work correctly?
const inputPassword = (e) => {
setState(({ ...state, password: e.target.value }));
};
const inputPasswordRE = (e) => {
setState({ ..state, re_password: e.target.value });
if (password === re_password) {
alert(`SUCCESS`)
} else {alert(`ERROR`)}
};
You're right, setState is updates state asynchronously, so it does not guaranty change immediately, you can use following approach to do your task.
Using Class based
const inputPasswordRE = (e) => {
this.setState({ ..state, re_password: e.target.value }, (state) => {
if (state.password === state.re_password) {
alert(`SUCCESS`)
} else {
alert(`ERROR`)
}
});
};
OR
You can also use `componentDidUpdate` as well.
Using React Hooks
If you're using react hooks, you can use useEffect to detect the changes and do your validations.
useEffect(() => {
if (password === re_password) {
// Do your stuff here
}
}, [password, re_password])
You can use the following for class-based components.
componentDidUpdate(prevProps, prevState) {
if (this.state.password === this.state.rePassword) {
// passwords matched
} else {
// passwords didn't match
}
}
You can also use function-based component with a useState hook.
Check out the following code
import React, {useState, useEffect} from 'react'
export default function func() {
const [password, setPassword] = useState('');
const [rePassword, setRePassword] = useState('');
useEffect(() => {
if (password === rePassword){
// passwords matched
} else {
// passwords don't match
}
})
return (
<div>
<input type="text" name="password" type="password" onClick={(e) => setPassword(e.target.value)} />
<input type="text" name="password" type="password" onClick={(e) => setRePassword(e.target.value)} />
</div>
);
}
This should work perfectly.
Comment if you wanna know anything else.
Related
I want to block "-" (dash) from entering into the input field by the user. The "-" code is 189.
Here's my code :
import React, { useEffect, useState } from "react"
interface Props {
value: string
}
function InputFormat(props: Props) {
const [stateValue, setStateValue] = useState(() => value)
const handleChange = (event: any) => {
setStateValue(event.target.value)
}
const handlekeyDown = (event: any) => {
if (event.keycode === 189) {
return
}
}
return (
<>
<input
onKeyDown={(e) => handlekeyDown(e)}
value={stateValue}
onChange={(e) => handleChange(e)}
/>
</>
)
}
export default InputFormat
This is not working as expected. I am trying to return if the keycode is 189 form dash, but still I'm able to type. How can I achieve this here (notice this is a controlled component).
First of all, KeyboardEvents do not have a keycode. They have a deprecated keyCode (will be removed at some point).
Possible replacements for your check:
event.key === '-'
['Minus', 'NumpadSubtract'].includes(event.code)
Secondly, to prevent the change in <input />'s value, you have to call preventDefault() method on event:
const handleKeyDown = (event: KeyboardEvent) =>
event.key === "-" && event.preventDefault()
Side note: preventing default on user input is considered detrimental UX. The user does not feel respected. Disabling keyboard user input in <input/>s has been demonstrated to increase bounce rate and decrease user loyalty of web-pages.
A more respectful way of telling them they're not allowed particular values in <input /> is to display an invalidity message about the current value, coupled with some visual indicator (red border, etc...). Or, as #DanielBeck pointed out in the comments, to simply disregard the dash where you're consuming it (without overriding the user input's value).
Check event.key (keyCode is deprecated) and use event.preventDefault() to stop the character from being entered.
const handlekeyDown = (event) => {
if (event.key === '-') {
event.preventDefault();
}
}
Well, you could return if the value is - in your handleChange function.
const handleChange = (event: any) => {
if(event.target.value == '-') {
return;
}
setStateValue(event.target.value)
}
Listening keyDown and use preventDefault it is not the best way.
A more attractive solution is move condition to onChange:
import React, { useEffect, useState } from "react"
interface Props {
value: string
}
function InputFormat(props: Props) {
const [stateValue, setStateValue] = useState(() => value)
const onChangeHandler = (e) => {
if (e.target.value.includes('-')) return
setValue(e.target.value)
}
return (
<>
<input
value={stateValue}
onChange={onChangeHandler}
/>
</>
)
}
export default InputFormat
The best solution it will be if you modify this code with regex. It would be much more scalable.
const noDashesRegex = /[-]+/
if (noDashesRegex.test(e.target.value)) return
Ideally you'd either use a regular expression or just includes(), but
if you insist on using a keycode, you just convert the character to a keycode, and if any of the characters within your input string match your key code, then return, else, update your state:
Code sandbox example, but I'm testing for "e" instead of the dash:
https://codesandbox.io/s/upbeat-forest-7qxlub?file=/src/App.js
import "./styles.css";
import { useState } from "react";
export default function App() {
const [field, setField] = useState("");
const sanitizeInput = (e) => {
for (let i = 0; i < e.target.value.length; i++) {
let code = e.target.value.charCodeAt(i) - 32;
if (code === 69) {
return;
}
}
setField(e.target.value);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<input value={field} onChange={(e) => sanitizeInput(e)} />
</div>
);
}
I have the following setup in my react app:
const [modalReasonInput, setModalReasonInput] = useState("")
const validateReason = () => {
console.log(modalReasonInput)
if (modalReasonInput === "" && record.judgement_result === "fail") {
setError("Field is required")
return false
}
setError("")
return true
}
const handleChange = (value) => {
setModalReasonInput(value)
validateReason()
}
<TextField
type="textarea"
maxLength={1000}
showCounting
rows={6}
value={modalReasonInput}
onBlur={() => validateReason()}
onChange={(e) => handleChange(e.target.value)}
/>
With the above code when I enter first character in the input field a blank/white space is set as value to state modalReasonInput.
The value is set only if I type another character, but value is set to first character I had typed.
Due to this my validation is failing while typing first character.
How can I update the state value as I type.
It is getting updated as you type but when you try to console.log the state right after you call setModalReasonInput you can't see it right away because state updates in React is asynchronous. You can check the state with useEffect as the state changes:
useEffect(() => {
console.log(modalReasonInput)
}, [modalReasonInput])
The problem is your handleChange function. If you set the state and perform a validation shortly after it is not guaranteed that the state is available in the next line.
const handleChange = (value) => {
setModalReasonInput(value)
validateReason() // value not guarenteed to be in modalReasonInput state
}
You can either separate state update and validation or directly pass in your value to your validation function validateReason():
const validateReason = (value) => {
console.log(value)
if (value === "" && record.judgement_result === "fail") {
setError("Field is required")
return false
}
setError("")
return true
};
const handleChange = (value) => {
setModalReasonInput(value)
validateReason(value)
}
const [modalReasonInput, setModalReasonInput] = useState("")
const reasonRef = useRef("")
const validateReason = () => {
console.log(easonRef.current)
if (reasonRef.current === "" && record.judgement_result === "fail") {
setError("Field is required")
return false
}
setError("")
return true
}
const handleChange = (value) => {
setModalReasonInput(value)
easonRef.current = value
validateReason()
}
Thanks everyone I ended up using useRef hook for this issue. I didn't use #sm3sher approach because validateReason is being called by other methods also which do not get event value.
I didn't use #Enes method useEffect because judgement_result is initially set to null.
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]);
}
}
on first button press onChange method function is called but state is not updating as it should and on second button press it is updating see this
import React,{useState} from 'react';
function MainHeader(props) {
const [FirstName, setFirstName] = useState('')
const [User, setUser] = useState({
FirstName: '',
LastName: ''
})
const nameOnChange = (event) => {
setFirstName(event.target.value)
console.log(FirstName)
}
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
console.log(User)
props.addUserToFirebase(User)
}
return (
<div>
<h1>Checking</h1>
<input type="text" onChange={(e) => nameOnChange(e)} value={FirstName} />
<button onClick={() => addName()}>Enter</button>
</div>
);
}
on nameOnChange it is console.log(FirstName) when first time a pressed something then console logs empty state (initial state) and on second button it updates the previous button pressed. I have tried creating class component as well but i am seeing the same issue , same thing happens in the addName function it updates state on second click .
see console
I don't see a problem here. It is working perfectly as it should be. But the only problem I see is your wrong understanding of how React works or how Functional Programming works in general.
There's no mutation in Functional Programming
const nameOnChange = (event) => {
// event => new value
// FirstName => old value
// they remain that way throughout this function call
setFirstName(event.target.value)
// even if you set the state, the values won't change
// they will be updated only in next function call
console.log(FirstName) // still old value
}
The same goes for addName()
For each re-render React call the function MainHeader with values that will not be mutated throughout their call or life. When value are updated, React will call MainHeader with the updated the values.
Correct way of using your Component
Works, but not better way
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
//
console.log({
...User,
FirstName: FirstName
}) // new value, since User is not mutated, User will still have the old value
props.addUserToFirebase(({
...User,
FirstName: FirstName
})
}
Better way
Always use useEffects for side effects.
// Just set the state
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
}
// handle side effects here
useEffect(() => {
// check is needed here, since it will be
// called on component's first mount
if(User.FirstName !== ''){
console.log(User)
props.addUserToFirebase(User)
}
}, [User])
// This will be called whenever React detects a change in `User`
https://reactjs.org/docs/hooks-effect.html
You must use useEffect().
const nameOnChange = (event) => {
setFirstName(event.target.value)
}
useEffect(()=>{
if(FirstName !== ''){
console.log(FirstName)
}
}, [FirstName])
const addName = () => {
setUser({
...User,
FirstName: FirstName
})
}
useEffect(()=>{
if(User.FirstName !== '' && User.LastName !== ''){
console.log(User)
props.addUserToFirebase(User)
}
}, [User])
I've dug myself into a deep rabbit hole with this component in an attempt to use React hooks.
The Parent component handles a dictionary state which is eventually distributed to multiple components.
My problem child component WordInput has a form with a single input. When submitting the form the component is fetching the word's definition from an API and passing on both the word and the definition to the parent which then sets the state in the form of dictionary. So far, so good IF it's the first word in dictionary. The part I'm having trouble with is to submit any subsequent words/definitions.
When the user submits a subsequent word, I want the component to check whether the word already exists in the dictionary that is passed to the child. If it doesn't exist, add it to the dictionary via the submit function.
I think the problem is that I'm trying to do too much with useEffect
I useEffect to:
- set loading
- check and process the dictionary for existing words
- check that definition and word aren't empty and submit both to parent/dictionary
- fetch a definition from an API
In the unprocessed code, I have multiple console.groups to help me keep track of what is happening. The more I add to the component, the more Subgroups and subgroups of subgroups accumulate. Clearly, the approach I'm taking isn't very dry and causes too many re-renders of the component/useEffect functions. For conciseness, I have taken out the console.log entries.
The imported fetchWordDefinition merely processes the fetched data and arranges it correctly into an array.
I don't know how to keep this dry and effective, and any help is appreciated with this rather simple task. My hunch is to keep all the logic to submit the word/definition in the submit handler, and only use useEffect to validate the data prior to that.
import React, { useState, useEffect } from "react";
import fetchWordDefinition from "./lib/utils";
const WordInput = ({ onSubmit, dictionary }) => {
const [definition, setDefinition] = useState([]);
const [cause, setCause] = useState({ function: "" });
const [error, setError] = useState({});
const [loading, setLoading] = useState(false);
const [word, setWord] = useState("");
const [wordExistsInDB, setWordExistsInDB] = useState(false);
useEffect(() => {
const dictionaryEmpty = dictionary.length === 0 ? true : false;
if (dictionaryEmpty) {
return;
} else {
for (let i = 0; i < dictionary.length; i += 1) {
if (dictionary[i].word === word) {
setWordExistsInDB(true);
setError({ bool: true, msg: "Word already exists in DB" });
break;
} else {
setWordExistsInDB(false);
setError({ bool: false, msg: "" });
}
}
}
}, [dictionary, word]);
useEffect(() => {
const definitionNotEmpty = definition.length !== 0 ? true : false;
const wordNotEmpty = word !== "" ? true : false;
if (wordNotEmpty && definitionNotEmpty && !wordExistsInDB) {
onSubmit(word, definition);
setWord("");
setDefinition([]);
}
}, [definition, word, onSubmit, wordExistsInDB]);
useEffect(() => {
if (cause.function === "fetch") {
async function fetchFunction() {
const fetch = await fetchWordDefinition(word);
return fetch;
}
fetchFunction().then(definitionArray => {
setDefinition(definitionArray);
setCause({ function: "" });
});
}
}, [cause, word]);
const handleSubmit = async e => {
e.preventDefault();
setLoading(true);
setCause({ function: "fetch" });
};
return (
<form onSubmit={handleSubmit}>
{error.bool ? <span>{error.msg}</span> : null}
<input
name='word'
placeholder='Enter Word'
type='text'
value={word}
onChange={({ target: { value } }) => setWord(value)}
/>
<input type='submit' />
</form>
);
};
export default WordInput;
There are indeed more useEffect's happening than necessary, as well as most of the state. All you need is the handleSubmit to do the fetching.
const WordInput = ({ onSubmit, dictionary }) => {
const [word, setWord] = React.useState("");
const handleChange = React.useCallback(e => {
setWord(e.currentTarget.value)
}, [])
const handleSubmit = React.useCallback(() => {
//check if word is in dictionary
const wordIsAlreadyThere = dictionary.map(entry => entry.word).includes(word)
//fetch the definition, wait for it, and call submit
if(!wordIsAlreadyThere && word.length > 0){
fetchWordDefinition(word)
.then(definition => {
onSubmit(word, definition)
setWord('')
}).catch(err => console.log(err))
}
}, [])
return (
<form onSubmit={handleSubmit}>
<input
value={word}
onChange={handleChange}/>
<input type='submit' />
</form>
);
}
I think you're missing out on some clarity and what useEffect is for
A functional component gets re-ran everytime either a prop or a state changes. useEffect runs when the component gets created, and we use it for things like doing a first-time fetch, or subscribing to an event handler. The second argument (array of variables) is used so that, if we have for example a blog post with with comments etc, we don't re-fetch everything unless the ID changes (meaning it's a new blog post)
Looking at your code, we have this flow:
User inputs something and hits Submit
Check if the word exists in a dictionary
a. If it exists, display an error message
b. If it doesn't exist, fetch from an API and call onSubmit
So really the only state we have here is the word. You can just compute an error based on if the word is in the dictionary, and the API call is done in a callback (useCallback). You have a lot of extra state that doesn't really matter in a state-way
A simplified version would look like this
const WordInput = ({ onSubmit, dictionary }) => {
const [word, setWord] = useState("")
const [loading, setLoading] = useState(false)
// `find` will find the first entry in array that matches
const wordExists = !!dictionary.find(entry => entry.word === word)
// Ternary operator,
const error = (wordExists) ? "Word already exists in DB" : null
// When user hits submit
const handleSubmit = useCallback(() => {
if (wordExists || !word.length) return;
setLoading(true)
fetchFunction()
.then(definitionArray => {
onSubmit(word, definitionArray)
})
}, [])
return (
<form onSubmit={handleSubmit}>
{error && <span>{error}</span>}
<input
name='word'
placeholder='Enter Word'
type='text'
value={word}
onChange={({ target: { value } }) => setWord(value)}
/>
<input type='submit' onclick={handleSubmit} disabled={wordExists}/>
</form>
);
};
Your component only needs to keep track of the word and the loading flag.
When the user changes the word input it updates the word state.
When the user submits the form the loading state changes. This triggers a useEffect that will first check if the word already exists. If not it proceeds to fetch it and add both the word and its definition to the dictionary.
const WordInput = ({ onSubmit, dictionary }) => {
const [loading, setLoading] = useState(false);
const [word, setWord] = useState("");
useEffect(() => {
if (!loading) return;
const existing_word = dictionary.find(item => item.word === word);
if (existing_word) return;
const fetchFunction = async () => {
const definition = await fetchWordDefinition(word);
// Update the dictionary
onSubmit(word, definition);
// Reset the component state
setWord("");
setLoading(false);
};
fetchFunction();
}, [loading]);
return (
<form
onSubmit={e => {
e.preventDefault();
if (word.length) {
setLoading(true);
}
}}
>
<input
name="word"
placeholder="Enter Word"
type="text"
value={word}
onChange={({ target: { value } }) => setWord(value)}
/>
<input type="submit" />
</form>
);
};
Please let me know if something is not clear or I missed something.