Handling errors from api with Formik - reactjs

I am catching errors from api and showing them in form, and that is working fine. But the problem is when I change one field in form all errors disappear. For form I am using Formik and for validation Yup.
const handleSubmit = (values, {setSubmitting, setFieldError, setStatus}) => {
someApiCall(values)
.then(
() => {
},
(error) => {
// example of setting error
setFieldError('email', 'email is already used');
})
.finally(() => {
setSubmitting(false)
});
};
I tried with adding third parametar false to setFieldError, but nothing changed.

Here's my working example: https://codesandbox.io/s/formik-example-dynamic-server-rendered-values-1uv4l
There's a callback validate available in Formik: https://jaredpalmer.com/formik/docs/api/formik#validate-values-values-formikerrors-values-promise-any using which you can probably try to do something like below.
I initiated emailsAlreadyInUse with empty array and then in your API call once error gets returned then add that user to the array and once user uses the same email again and tried to validate, although it will pass Yup validation but it will be caught in validate callback which I believe runs after Yup validation (though I could be wrong but in your case doesn't matter).
const emailsAlreadyInUse= [];
const handleSubmit = (values, {
setSubmitting,
setFieldError,
setStatus
}) => {
someApiCall(values)
.then(
() => {
// Do something
// possibly reset emailsAlreadyInUse if needed unless component is going to be unmounted.
},
(error) => {
// example of setting error
setFieldError('email', 'email is already used');
// Assuming error object you receive has data object that has email property
emailsAlreadyInUse.push(error.data.email);
})
.finally(() => {
setSubmitting(false)
});
};
<Formik
...
...
validate = {
values => {
let errors = {};
if (emailsAlreadyInUse.includes(values.email)) {
errors.email = 'email is already used';
}
return errors;
}
}
/>

I found simplier method to make API validation errors always visible than using validate method. You can set validateOnBlur and validateOnChange on your form to false. It will cause validation only on submit and errors returned from API will remain after changing field value.
Usage:
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validateOnBlur={false}
validateOnChange={false}
>
...form fields...
</Formik>

Related

how to get data from another api and post it with another api in react js

Hi i am working on reactjs with axios. currently i am working on post api and i am working on hybrid form like need some data from get api and some data send by input value. currently i am able to do only one thing. like get value in input field or post value from same input field.but i want to to same thing with one input field. like update field first get data and update it. but in update we use one api and here i m using two api
Post api
handleSubmit = event => {
event.preventDefault();
event.target.reset()
console.log(this.props.rowId);
let rId = this.props.rowId;
const posts = rId;
console.log(posts);
const brokerId = localStorage.getItem("brokerId");
// Based on several assumptions: single obj returned, posts is never empty
axios.post(`http://http:testapi/100/${brokerId}`, {
ipoId: this.props.rowId,
// question: this.state.question,
// answer: this.state.answer,
UCC: this.state.UCC,
NAME: this.state.NAME,
ZONE: this.state.ZONE,
UPDATED_BY: this.state.UPDATED_BY,
// faqList: [
// {
// }
// ]
}).then(res => {
this.getData();
this.setState({
question: ''
})
this.setState({
answer: ''
})
})
.catch(err => {
this.setState({
errorMessage: err.message
});
})
}
handleNAMEChange = event => { this.setState({ NAME: event.target.value }) }
handleUCCChange = event => { this.setState({ UCC: event.target.value }) }
Get Api
getData() {
let rId = this.props.rowId;
axios.get(`http:testapi/100/${rId}`)
.then(res => {
const faq = res.data.body.data.client_detail;
this.setState({
faq
});
})
};
able to get the data from get api in this field
<TextField
type="text"
label="UCC"
value={faqList.clienT_CODE}
name="UCC"
onChange={this.handleUCCChange}
/>
able to post the data from post api in this field
<TextField
type="text"
label="NAME"
value={this.state.NAME}
name="UCC"
onChange={this.handleNAMEChange}
/>

Where to place dispatch function for useReducer and why?

I'm new to React and I'm currently learning about useReducer.
I've created a simple login feature that verifies if the user inputted email includes '#' and the password length is greater than 5.
If these two conditions are met, I want my program to display an alert with success or fail message when pressing on the submit button.
What I'm curious about is that the application displays "Success" on submit when I add dispatch({type: 'isCredValid')} in useEffect(commented out in the code below), but the application displays "fail" when I add the dispatch({type: 'isCredValid'}) in the onSubmit handler without using useEffect. I was expecting the application to display "Success" when adding the dispatch({type: 'isCredValid')} in the onSubmit handler without the help of useEffect. Why is it not displaying "Success"? And why does my application display "Success" when the dispatch function is in the useEffect?
Reducer function :
const credReducer = (state, action) => {
switch(action.type) {
case 'email' :
return {...state, email: action.value, isEmailValid: action.value.includes('#')};
case 'password' :
return {...state, password: action.value, isPasswordValid: action.value.length > 5 ? true : false};
case 'isCredValid' :
return {...state, isCredValid: state.isEmailValid && state.isPasswordValid ? true : false};
default :
return state;
}
}
Component and input handlers
const Login = () => {
const [credentials, dispatch] = useReducer(credReducer, {
email: '',
password: '',
isEmailValid: false,
isPasswordValid: false,
isCredValid: false
})
// useEffect(() => {
// dispatch({type: 'isCredValid'})
// }, [credentials.isEmailValid, credentials.isPasswordValid])
const handleSubmit = (e) => {
e.preventDefault()
dispatch({ type: "isCredValid" })
if (credentials.isCredValid === true) {
alert ("Success!")
} else {
alert ('failed')
}
}
const handleEmail = (e) => {
dispatch({ type: "email", value: e.target.value })
}
const handlePassword = (e) => {
dispatch({ type: "password", value: e.target.value })
}
return (
<Card className={classes.card}>
<h1> Login </h1>
<form onSubmit={handleSubmit}>
<label>Email</label>
<input type="text" value={credentials.email} onChange={handleEmail}/>
<label>Password</label>
<input type="text" value={credentials.password} onChange={handlePassword}/>
<button type="submit"> Submit </button>
</form>
</Card>
)
}
if (credentials.isCredValid === true) {
alert ("Success!")
} else {
alert ('failed')
}
You are probably referring to above alert that you didn't immediately see "Success". That doesn't happen like that, just like with updating state, when you dispatch something, you will see the update on the next render.
This useEffect may work, but you're kind of abusing the dependency array here. You're not actually depending on credentials.isEmailValid or credentials.isPasswordValid. You should use these dependencies to decide which action to dispatch, and maybe that's your plan already.
The reason your handleSubmit doesn't seem to work, is what others point out. You won't be able to see the result until next render, so not inside the handleSubmit function.
// useEffect(() => {
// dispatch({type: 'isCredValid'})
// }, [credentials.isEmailValid, credentials.isPasswordValid])
const handleSubmit = (e) => {
e.preventDefault()
dispatch({ type: "isCredValid" })
if (credentials.isCredValid === true) {
alert ("Success!")
} else {
alert ('failed')
}
}
To see the results, add another useEffect and trigger the alert from there:
useEffect(() => {
if(credentials.isCredValid){
alert('Success!')
}
}, [credentials])

react-hook-form's validate function always returns errors

This is the one that occurs problem.
<input
type="text"
name="nickname"
{...register('nickname', {
required: true,
validate: async (value) =>
value !== profile?.nickname &&
(await validateNickname(value).catch(() => {
return 'Invalid nickname.';
})),
})}
placeholder="Nickname"
/>
If the input value is not same with the defaultValue
and It's duplicated with another nickname, (validateNickname function returns error)
Then, error message is registered 'Invalid nickname.'
and the errors object looks like this below.
{
nickname:
message: "Invalid nickname."
ref:...
type: "validate"
}
but the problem is
If I input the value which is same with the defaultValue,
or If I input not duplicated value,
The errors object should be empty.
but it still returns error like this below.
{
nickname:
message: ""
ref:...
type: "validate"
}
so there's no error message registered, but somehow, error is exist.
Please let me know if there's anything I'm doing wrong.
Thanks!
This could be the problem:
(await validateNickname(value).catch(() => {
return 'Invalid nickname.';
}))
You want to return a boolean in your AND expression, but you are returning a string, which would be saying the validation is correct.
If you want to handle that validation error so as to display the message in html you can do a simple error handling approach like this one (using named validations):
// ... your validation set up
validate:{
validateNickname_ : async (value) => {
let response = false;
await validateNickname(value)
.then( () => { response = true;})
.catch(() => { response = false;});
return value !== profile?.nickname && response;
}
}
// ... showing your error message
{
errors.nickname && errors.nickname.type=="validateNickname_" &&
<p className='error-form-msg'>
{"Your custom error message here ..."}
</p>
}

How to customer data in value antd and Put axios

How to customer data in value antd and Put axios
https://ant.design/components/form/
The input does not let me change the data that will be updated by axios
I must still be able to fix it.
I want to show Antd form data. I read the review and use initialValues But I still can't.
componentDidMount () {
let id = this.props.match.params.id;
httpClient
.get(`http://localhost:8085/api/v1/customer/customer/${id}`)
.then((e) => this.setState({
cus_fname: e.data.cus_fname,
cus_lname: e.data.cus_lname,
cus_email: e.data.cus_email,
cus_tel: e.data.cus_tel,
cus_car_number: e.data.cus_car_number,
cus_band: e.data.cus_band,
cus_address: e.data.cus_address,
}));
}
onFinish = async(values) => {
// console.log("ค่า values ที่มาจาก form: ", values);
const formData = new FormData();
formData.append("cus_fname", values.cus_fname);
formData.append("cus_lname", values.cus_lname);
formData.append("cus_email", values.cus_email);
formData.append("cus_tel", values.cus_tel);
formData.append("cus_car_number", values.cus_car_number);
formData.append("cus_band", values.cus_band);
formData.append("cus_address", values.cus_address);
await httpClient
.put(`http://localhost:8085/api/v1/customer/customer/`, formData )
.then((res) => {
// console.log(res.data);
// console.log( "PushData to server success : ", res);
}).catch((error) => {
console.log("Error :", error);
});
await this.props.history.goBack();
};
handleChange = event => {
event.persist();
this.setState({
[event.target.fname]: event.target.value
});
};
render() {
const { fname, lname, email, phone, band, cus_address, car_number} = this.state;
return (
<Form {...layout} name="nest-messages" onFinish{this.updateCustomer} >
<FormItem
label="ชื่อ"
name="fname"
defaultValue={fname}
rules={[{ required: true, message: "โปรดระบุชื่อ ", }]}
onChange={this.handleChange}
>
<Input />
</FormItem>
)
initialValues will show only the state value on the first render, it will not change the value of the form on componentDidMount even you did setState because that will be a second render.. to achieve it you need a reference to the form and use the api of the instance.
//create instance
formRef = React.createRef();
componentDidMount () {
let id = this.props.match.params.id;
httpClient
.get(`http://localhost:8085/api/v1/customer/customer/${id}`)
.then((e) => {
this.formRef.current.setFieldsValue({
cus_fname: e.data.cus_fname,
cus_lname: e.data.cus_lname,
cus_email: e.data.cus_email,
cus_tel: e.data.cus_tel,
cus_car_number: e.data.cus_car_number,
cus_band: e.data.cus_band,
cus_address: e.data.cus_address,
});
//you can still do a setState
/*this.setState({
cus_fname: e.data.cus_fname,
cus_lname: e.data.cus_lname,
cus_email: e.data.cus_email,
cus_tel: e.data.cus_tel,
cus_car_number: e.data.cus_car_number,
cus_band: e.data.cus_band,
cus_address: e.data.cus_address,
});*/
})
}
and in the Form component:
<Form ref={this.formRef} {...layout} name="nest-messages" onFinish={this.updateCustomer} >
...
</Form>
take note that there is no onChange callback on <Form.Item> component on antd documentation. If you want to change the state on every change, use onValuesChange props of the Form.
<Form onValuesChange={(changedValues, allValues) => {
/* perform setState */
}}>
</Form>
You may also want to look on this link for more info.

How to set dynamic error messages in Yup async validation?

I am trying async validation in Formik using Yup's .test() method and need to set the error message that I get from the API. Error messages are going to be different based on some conditions in backend.
Tried few solutions mentioned here
https://github.com/jquense/yup/issues/222 and Dynamic Validation Messages Using Yup and Typescript
But Yup is throwing the default error message given in test().
Documentation says that
All tests must provide a name, an error message and a validation function that must return true or false or a ValidationError. To make a test async return a promise that resolves true or false or a ValidationError.
I am resolving a new ValidationError with the error message but still, it throws the default error.
Here is the code.
const schema = Yup.object().shape({
email: Yup.string().test(
"email_async_validation",
"Email Validation Error", // YUP always throws this error
value => {
return new Promise((resolve, reject) => {
emailValidationApi(value)
.then(res => {
const { message } = res.data; // I want this error message to be shown in form.
resolve(new Yup.ValidationError(message));
})
.catch(e => {
console.log(e);
});
});
}
)
});
I got it working using the function syntax instead of arrow function for validation function.
Doc says:
test functions are called with a special context, or this value, that
exposes some useful metadata and functions. Note that to use this
context, the test function must be a function expression (function test(value) {}), not an arrow function, since arrow functions have
lexical context.
Here is the working code.
const schema = Yup.object().shape({
email: Yup.string()
.email("Not a valid email")
.required("Required")
.test("email_async_validation", "Email Validation Error", function (value) { // Use function
return emailValidationApi(value)
.then((res) => {
const message = res;
console.log("API Response:", message);
return this.createError({ message: message });
// return Promise.resolve(this.createError({ message: message })); // This also works
})
.catch((e) => {
console.log(e);
});
})
});
Actually you're almost correct.You just need to use the following:
resolve(this.createError({ message: message }));
Let me know if it still doesn't work ya
I am able to do this with arrow function as well.
const schema = Yup.object().shape({
email: Yup.string()
.email("Not a valid email")
.required("Required")
.test("email_async_validation", "Email Validation Error", (value, {createError}) {
return emailValidationApi(value)
.then((res) => {
const message = res;
console.log("API Response:", message);
return createError({ message: message });
})
.catch((e) => {
console.log(e);
});
})
});
Don't pass second parameter, as we generally pass it as a error message instead create your own custom message using "createError" and return it on your condition.
import * as yup from "yup";
const InitiateRefundSchema = yup.object().shape({
amountPaid: yup.number(),
refundAmount: yup
.number()
.test("test-compare a few values", function (value) {
let value1 = this.resolve(yup.ref("amountPaid"));
let value2 = this.resolve(yup.ref("refundAmount"));
if (value1 < value2) {
return this.createError({
message: `refund amount cannot be greater than paid amount '${value1}'`,
path: "refundAmount", // Fieldname
});
} else return true;
}),
})
With Internatilization ('vue-i18n') and Yup ('yup') options you could use as:
const fullname = yup.string().required(this.$t('validate.required')).min(8, this.$t('validate.min', {min: '8'}))
So with this line below, the text error message will be changed as the locale option change.
this.$t('validate.min')
pt.ts
validation: {
required: 'Esse campo é obrigatório',
size_min: 'Deve no minimo {min} caracteres',
}
en.ts
validation: {
required: 'Required please',
size_min: 'Please only {min} caracteres',
}

Resources