Code below is a simplified version of the reality.
It almost works. The issue is in my AddSomething component the value of the snackbar is always one value behind. I've used console logs as you can see in the code.
It goes like this:
"inside validateName"
"error is set to .... " (inside validateName)
"ELSE" (inside addSomething)
So it does everything in correct order. But the error in AddSomething is always one value behind.
So first time I input an empty string and the error message displayed is the default ''
(It should display 'Name can not be below 1')
Second time I try with a string above > 50 characters and it displays the old value of 'Name can not be below 1' but it should be: 'Name can not be above 50'
And it keeps going like this. How can I change the code to actually get the current value?
const AddSomething = () => {
const [error, validate] = useValidation()
const handleSave = async () => {
if(await validate(valuesToBeSaved){
saveItems()
}else {
console.log("ELSE")
setSnackBarError({
message: error
})
}
}
}
function useValidation() {
const [error, setError] = useState('')
let valid = true
async function validate(valuesToBeSaved: string[] | number[]) {
for(const value in valuesToBeSaved) {
if(typeof value === 'string'){
valid = await validateName(value)
}
if(valid === false) return false
}
}
async function validateName(name: string): Promise<boolean> {
console.log("inside validateName")
let valid = true
await nameSchema
.validate({ name })
.then((err) => {
valid = true
})
.catch((err) => {
console.log("error is set to: " + err.mesage)
setError(err.message)
valid = false
})
return valid
}
return [error, validate]
}
const nameSchema = yup.object({
name: yup
.string()
.min(1, 'Name can not be below 1')
.max(50, 'Name can not be above 50')
.required('Name is required'),
})
Related
I am trying to create some custom error validation in React
I have a values obj in state and an error obj in state that share the same keys
const [values, setValues] = useState({
name: "",
age: "",
city: ""
});
const [err, setErr] = useState({
name: "",
age: "",
city: ""
});
i have a very simple handle change and an onSubmit which i want to run my custom validator function inside
const handleChange = (e) => {
setValues({
...values,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault();
validateForms();
};
in my validateForms function my theory is since both my pieces of state share the same keys I am trying to see if any of those values === '' if yes match is the same key in the err obj and set that respective value to the error and then do other stuff in JSX
const validateForms = () => {
for (const value in values) {
if (values[value] === "") {
setErr({
...err,
value: `${value} is a required field`
});
}
}
};
I definitely feel like I'm not using setErr properly here. Any help would be lovely.
link to sandbox: https://codesandbox.io/s/trusting-bartik-6cbdb?file=/src/App.js:467-680
You have two issues. First, your error object key needs to be [value] rather than the string value. Second, you're going to want to use a callback function in your state setter so that you're not spreading an old version of the error object:
const validateForms = () => {
for (const value in values) {
if (values[value] === "") {
setErr(err => ({
...err,
[value]: `${value} is a required field`
}));
}
}
};
A more intuitive way to set errors might be to accumulate them all and just set the error state once:
const validateForms = () => {
const errors = {};
for (const value in values) {
errors[value] = values[value] === "" ? `${value} is a required field` : "";
}
setErr(errors);
};
Below function always submit the form at the first render of the component even if firstName and lastName fields are empty.
const [formErrors, setFormErrors] = useState({});
const registerUser = () => {
if (firstName === "") {
setFormErrors((prev) => {
return { ...prev, firstName: "Firstname is required!" };
});
}
if (lastName === "") {
setFormErrors((prev) => {
return { ...prev, lastName: "Lastname is required!" };
});
}
if (Object.keys(formErrors).length === 0) {
console.log("Form Submitted");
} else {
console.log("Failed");
}
}
At first render, the value of Object.keys(formErrors).length is 0, but the errors are shown on the screen, weird!.
I tried to use useRef which worked, but it doesn't display the errors on screen because the component doesn't rerender with useRef.
How do I prevent this form from being submitted if errors still exist?
Thank You
Your problem is that you have called set state (formErrors) and then you try to read it immediately. So when you set it, it hasn't been updated yet, you are reading stale data.
To fix it use some local variable in that function (initially you may want to initialize it with state data) to keep track of errors, and then at the end when you are done with it, put that in state.
Something like this
const registerUser = () => {
let errors = {
...formErrors
}
if (firstName === "") {
errors.firstName = "Firstname is required!"
}
if (lastName === "") {
errors.lastName = "lastName is required!"
}
if (Object.keys(errors).length === 0) {
console.log("Form Submitted");
} else {
console.log("Failed");
}
setFormErrors(errors)
}
I'm using the Axios in React to register a user into MongoDb database.
But before I register a user, I check if that user already exists in the database, but since axios.post() is asynchronous, the rest of the code precending this response executes and user with same Id is regsitered again.
How do I solve this. PFB my code:
const validateRegister = (values) => {
let errors={};
const patternName = new RegExp('^[a-zA-Z ]{3,20}$')
const patternPhone = new RegExp('^[0-9]{9,10}$')
const patternEmail = new RegExp('^[a-zA-Z0-9._:$!%-]+#[a-zA-Z0-9.-]+.[a-zA-Z]$')
const patternPassword = new RegExp('(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{8,})')
if(!values.name || !patternName.test(values.name)){
errors.name="Please enter a valid name"
}
if(!values.phone || !patternPhone.test(values.phone)){
errors.phone="Please enter a valid Phone number of 9-10 digits"
}
if(!values.email || !patternEmail.test(values.email)){
errors.email="Please enter a valid email address"
}
if(!values.password || !patternPassword.test(values.password)){
errors.password="Please enter a strong password to continue. A strong password has: Atleast 8 characters in length, 2 letters in upper case, 1 special character (!##$&*), 2 number (0-9), 3 letters in lower case"
}
if(!values.isTermsAndConditionsAccepted){
errors.isTermsAndConditionsAccepted = "Please Accept the Terms and conditions"
}
//Check if the user already exist
if(values.phone){
let formData = new FormData();
formData.append('phone', values.phone);
console.log('inside user check')
axios.post('http://localhost:3001/doesUserExistByPhone', formData).then(response => {
//Success - then create account
}).catch(errorResponse=>{
console.log(errorResponse)
if(errorResponse.response.status===409){
console.log('User already exist');
errors.phone="User Already exist. If you've already registered. Please try to Login.";
return errors;
}
else if(errorResponse.response.status===500){
errors.phone = "Unable to register user, contact SwapiFi Support";
return errors;
}
})
}
console.log('Errors found before creating user: ', errors);
return errors;
}
export default validateRegister
I invoke this Validator from another js file:
const useFormRegister = (submitForm) => {
const [errors, setErrors] = useState({});
const [dataIsCorrect, setDataIsCorrect] = useState(false);
const [values, setValues] = useState({
name: "",
phone: "",
email: "",
password: "",
isTermsAndConditionsAccepted: false
})
const handleValueChangeEvent = (event) => {
setValues({
...values,
[event.target.name] : event.target.value,
})
}
const handleRegisterEvent = (event) => {
console.log('Register button clicked');
event.preventDefault();
setErrors(validation(values));
console.log('Errors-Phone:', errors)
setDataIsCorrect(true);
}
useEffect(() => {
console.log('No. of errors:', Object.keys(errors).length)
{Object.entries(errors).map(([key, value]) => (
console.log("Error, ", key, ':', value)
))}
if(Object.keys(errors).length === 0 && dataIsCorrect){
submitForm(true);
let formData = new FormData();
{Object.entries(values).map(([key, value]) => (
formData.append(key, value)
))}
console.log(formData)
axios.post('http://localhost:3001/registerUser', formData).then(response => {console.log(response)}).catch(error=>{console.log(error)})
}
}, [errors])
return {handleValueChangeEvent, handleRegisterEvent, values, errors};
}
export default useFormRegister
You probably don't want to fire off a check synchronously. Look into async/await syntax. You can write code that "looks" synchronous but will actually execute asynchronously. This will allow you to do something like:
const checkUserExists = async (user) => {
const repsonse = await axios('/check/user/endpoint');
const user = await response.json();
return !!user;
}
const registerUser = async (user) => {
const repsonse = await axios('/register/user/endpoint');
const data = await response.json();
// do stuff here
}
and now you can implement whatever logic you need around these functions
useEffect(()=>{
async function deal(){
let data = await axios.get("http://localhost:8000")
setDeal(..)
}
deal()
},[])
This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 1 year ago.
state isn't reflecting change immediately causing me to have to fun onSubmit twice to get the form to submit
If you want to perform an action on state update, you need to use the useEffect hook, much like using componentDidUpdate in class components since the setter returned by useState doesn 't have a callback pattern
from Link to suggested question
But I'm Honestly confused on how to implement an on Submit into a use effect I'm sorry I'm new to react
const onSubmit = async(data) = > {
setNameError(nameValidation(data.name));
setphoneError(phoneNumberValidation(data.phoneNumber));
setEmailError(emailValidation(data.email));
setMessageError(messageValidation(data.message));
//Here is where I'm getting caught up, in react dev tools the above states are being set to false but below the noErrors Variable is still reading false after all conditions check true the no Errors is still getting the old values for some reason I even used a settimeout method.
let noErrors = (!nameError && !phoneError && !emailError && !messageError);
if (noErrors) {
try {
// const templateParams = {
// name: data.name,
// email: data.email,
// number: data.phoneNumber,
// message: data.message,
// };
// await emailjs.send(
// process.env.REACT_APP_SERVICE_ID,
// process.env.REACT_APP_TEMPLATE_ID,
// templateParams,
// process.env.REACT_APP_USER_ID
// );
reset();
toastifySuccess();
} catch (e) {
console.log(e);
}
}
};
const hasCharacter = /[a-zA-Z]/g;
export const nameValidation = function nameValidation(name) {
if (name.length > 30) {
return 'This field only accepts 30 characters';
}
if (name.length < 5) {
return 'This field requires five characters';
}
if (/\d/.test(name)) {
return ' This field cannot contain numbers';
}
if (!name.includes(' ')) {
return 'This Field Requires A Space';
}
return false;
};
export const phoneNumberValidation = (number) = > {
if (number.length !== 10) {
return 'A Phone Number Must be ten digits';
}
if (hasCharacter.test(number)) {
return 'A Phone Number Shouldnt Contain A Letter';
}
return false;
};
export const emailValidation = (email) = > {
if (email.length > 30) {
return 'This field only accepts 30 characters';
}
if (email.length < 5) {
return 'This field requires five characters';
}
if (!email.includes('#')) {
return 'Email Addresses require # Symbol';
}
return false;
};
export const messageValidation = (message) = > {
if (message.length > 500) {
return 'This field only accepts 500 characters';
}
if (message.length < 5) {
return 'This field requires five characters';
}
return false;
};
Here there are 2 ways to solve your issue.
Store error in local variable and use those variables to setState and check noError.
const onSubmit = async(data) = > {
const nameErr = nameValidation(data.name);
const phoneErr = nameValidation(data.phoneNumber);
const emailErr = nameValidation(data.email);
const messageErr = nameValidation(data.message);
setNameError(nameErr);
setphoneError(phoneErr);
setEmailError(emailErr);
setMessageError(messageErr);
let noErrors = (!nameErr && !phoneErr && !emailErr && !messageErr);
// rest of your code
}
use useEffect to calculate noErrors
const onSubmit = async(data) = > {
setNameError(nameValidation(data.name));
setphoneError(phoneNumberValidation(data.phoneNumber));
setEmailError(emailValidation(data.email));
setMessageError(messageValidation(data.message));
}
useEffect(() => {
const submitForm = async () => {
let noErrors = (!nameErr && !phoneErr && !emailErr && !messageErr);
if (noErrors) {
try {
// const templateParams = {
// name: data.name,
// email: data.email,
// number: data.phoneNumber,
// message: data.message,
// };
// await emailjs.send(
// process.env.REACT_APP_SERVICE_ID,
// process.env.REACT_APP_TEMPLATE_ID,
// templateParams,
// process.env.REACT_APP_USER_ID
// );
reset();
toastifySuccess();
} catch (e) {
console.log(e);
}
}
}
submitForm();
},[nameError, phoneError, emailError, messageError])
I've written the following test:
it('validates the first name cannot be blank', () => {
const { findByLabelText, getByText } = render(<Profile />);
const firstName = findByLabelText('first name');
firstName.value = '';
fireEvent.blur(firstName);
const error = getByText('First name is required');
expect(error).not.toBeNull();
});
After the test runs I get the error:
Unable to find the "window" object for the given node.
How do I get this test to pass?
So it turns out I was setting the value of first name the wrong way. In fact, in this case there is no need to set the first name, since it defaults to ''. The correct test implementation would be this:
it('validates the first name cannot be blank', async () => {
const { getByLabelText, getByText } = render(<Profile />);
const firstName = getByLabelText(/first name/i);
fireEvent.blur(firstName);
let error;
await waitFor(() => {
error = getByText('First name is required');
});
expect(error).not.toBeNull();
});