formik
formik copied to clipboard
[v2] Validation runs on old values after setFieldTouched
🐛 Bug report
Current Behavior
Calling setFieldTouched runs validation on old values.
Expected behavior
Validation should be called on new values.
Reproducible example
https://codesandbox.io/s/formik-codesandbox-template-yqrgc
I can get around this issue with replacing
props.setFieldValue("name", "John");
props.setFieldTouched("name", true);
with
props.setFieldValue("name", "John");
// Set `shouldValidate` to `false` to prevent validation
props.setFieldTouched("name", true, false);
// Call validation with the new values
props.validateField(name);
Additional context
Related problems I could find: #1977, #2025
Your environment
| Software | Version(s) |
|---|---|
| Formik | 2.0.6 |
| React | 16.12.0 |
| Browser | Google Chrome 78 |
| npm/Yarn | Yarn 1.19.0 |
| Operating System | Fedora 31 |
Not sure if the #2116 should have closed this issue or not, but i believe this is still a problem in 2.1.1
Reproducible example https://codesandbox.io/s/formik-codesandbox-template-yqrgc
Upgrading formik to 2.1.1 in the reproducible example in the OP still shows the same behaviour
Both setXXX will are called synchronously in your example. setFieldTouched thus doesn’t wait for setFieldValue. Because hooks are annoying af, there is no way for us to provide a promise or callback after the commit AFAIK. Thus my suggestion is then to call setFieldValue first and let it run validation as side effect and then call setFieldTouched but abort validation.
My point above is that this isn’t a bug, it’s how React works.
Both setXXX will are called synchronously in your example. setFieldTouched thus doesn’t wait for setFieldValue. Because hooks are annoying af, there is no way for us to provide a promise or callback after the commit AFAIK. Thus my suggestion is then to call setFieldValue first and let it run validation as side effect and then call setFieldTouched but abort validation.
Thank you for the explanation! I'm not sure it falls under the responsibilities of Formik since the behavior is the result of how React works, but maybe it would help people if it's documented in Formik so it's less confusing?
My point above is that this isn’t a bug, it’s how React works.
I understand that, but isn't there way, how to avoid this behavior if setFieldValue internally sets field is touched (maybe optionally)?
Or what is, in this case, the best way how to create custom Formik-hooked components that need to set the value (of course) as well as set they're touched - e.g. I don't want to validateOnChange, so validation runs when touched is set and therefore validation is run on old value.
Thanks for any insights
I'm running into something similar, but it's with validation? Can someone help since this was working fine in Formik 1.x, but is now validation in Formik 2.x never seems to work correctly when using setFieldValue.
I tried the recommendation of using setFieldValue, then setFieldTouched and then validateField, but the field I am using is still not valid for some reason.
My code looks like so (a custom Field.)
Form.validationSchema = Yup.object().shape({
size: Yup.string()
.required('Size is required.'),
});
<Select
options={options}
name={field.name}
value={foundOption}
onChange={(option) => {
form.setFieldValue(field.name, option.value);
form.setFieldTouched(field.name, true, false);
form.validateField(field.name);
onChange(option.value);
}}
onBlur={field.onBlur}
In this case, 'Size is required' is showing up, even though I am setting a new value. The only time it starts to validate correctly is when I click away from the <Select> and then it finally validates.
But why is it not validating when I select an option?
If I print out the formik values, they look like so (after selecting an option.). It goes from invalid, valid to invalid again.
values: {name: "asfasf", size: "Small"}
errors: {size: "Size is required."}
isSubmitting: false
isValidating: false
isValid: false
dirty: true
then another render which makes it suddenly valid?:
values: {name: "asfasf", size: "Small"}
errors: {}
touched: {name: true, size: true}
status: undefined
isSubmitting: false
isValidating: false
isValid: true
dirty: true
Then another render which suddenly makes it invalid?
values: {name: "asfasf", size: "Small"}
errors: {size: "Size is required."}
touched: {name: true, size: true}
status: undefined
isSubmitting: false
isValidating: false
isValid: false
dirty: true
I am unsure how to fix this, but my issue was if I enabled validateOnMount, it is running the Yup schema on initialValues instead of the actual values.
I think I may open another bug report since I think it is unrelated to this.
Looks like this is what I am running into:
https://github.com/jaredpalmer/formik/issues/2046
My solution looks like this for form-level validation
const name = 'name';
const value = 'John';
form.setFieldValue(name, value);
form.setFieldTouched(name, true, false);
form.validateForm({ ...form.values, [name]: value });
So anywhere I'm using Formik 2.x and I use setFieldValue() or setValue() with the form I use the following component to wrap the Formik component's children to correctly validate the form. Is this the best workaround?
import React, {useEffect} from 'react'
import {useFormikContext} from 'formik'
const FormikValidate = ({children}) => {
const {values, validateForm} = useFormikContext()
useEffect(() => {
validateForm()
}, [values, validateForm])
return <>{children}</>
}
export default FormikValidate
If you have that option you can call setFieldTouched from onBlur handler. It did fix this issue with react-select
you can use the following, it worked for me
setTimeout(() => setFieldTouched(value, true))
Wrap setFieldTouched in a setTimeout() function
The setTimeout method fixed this issue for me with my React-Select fields.
you can use the following, it worked for me
setTimeout(() => setFieldTouched(value, true))Wrap
setFieldTouchedin asetTimeout()function
This is not a good idea. It works but, the component is looping.
My solution for not looping it's put this block code inside component:
useEffect(() => {
if (value) {
setTimeout(() => setFieldTouched(name, true, false));
setTimeout(() => validateForm({ ...values, [name]: value }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
@wallace-sf can you please provide a better code snippet of how you are using your code? I was going to create another component just to trigger validations but I see that you are using the specific value of the field that needs validation.
Thanks!
@wallace-sf can you please provide a better code snippet of how you are using your code? I was going to create another component just to trigger validations but I see that you are using the specific value of the field that needs validation.
Thanks!
Sure. Take a look of using a rating component (lib react-rating)
const IRating = fieldfy((props) => {
const {
field: { name, error, value },
readonly,
label,
setFieldTouched,
setFieldValue,
validateForm,
values,
} = props;
useEffect(() => {
if (value) {
setTimeout(() => setFieldTouched(name, true, false));
setTimeout(() => validateForm({ ...values, [name]: value }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
const handleClick = (rater) => {
setFieldValue(name, JSON.stringify(rater));
};
return (
<>
<div className="form-group custom-form-component-rating">
<h5 className="font-size-14">
<Markdown>{label}</Markdown>
</h5>
<Rating
readonly={readonly}
start={0}
stop={5}
step={1}
initialRating={parseInt(value, 10)}
name={name}
emptySymbol={<i className="far fa-star fa-2x" />}
fullSymbol={<i className="fas fa-star fa-2x" />}
onClick={(rater) => handleClick(rater)}
/>
{error ? (
<div className="col mt-1">
<i
className="fad fa-exclamation-triangle pr-1"
style={Styles.error_msg}
/>
<span style={Styles.error_msg}>{error}</span>
</div>
) : null}
</div>
</>
);
});
@wallace-sf thank you so much for the example. I have been pulling my hair out for almost a week now with validations doing strange things in various places after upgrade to 2.x.
Note that this only worked for me in 2.2.5 (2.2.1 did not work).
To the comments above this is how react works... While you are probably technically correct, this is such an unbelievable rake. Running validation with old values after setFieldValue has been called with new value is completely unexpected. This is something the framework should handle in my opinion or require developers to do on their own.
If we can work around the problem by adding hooks to our components there has be a way for formik to handle in a reliable way.
@gone-skiing agreed. "it's how react works" is not the final answer, but because the current Formik implementation relies on React hooks to manage state internally, a render is required to update the values passed to the callbacks, resulting in stale validations. Formik state should be separated from React hooks in a future major version, see my comment here: https://github.com/formium/formik/pull/2846#issuecomment-728225359
@johnrom thanks for the comment. It is great to see that the team is thinking about it.
@johnrom @jaredpalmer Because hooks are annoying af, there is no way for us to provide a promise or callback after the commit AFAIK. Agreed that this is annoying- making it possible have a callback or promise that runs after setFieldValue's state update is complete would be very useful. But the easy solution to that feels like simply not using hooks for that state in the Formik implementation. Regular class component setState offers the second (callback) parameter, which would enable exactly this. Is there a reason that isn't an option? Adding a callback/promise to APIs like setFieldValue, which receives the updated Formik state as a parameter (implemented internally using the callback parameter of class component setState) fees like the 80/20 solution to a lot of these kinds of issues. The lack of something like that has been a pretty major pain point for us in using Formik.
@leo-terratrue That would get you halfway there, but then you'd still have an issue with async validation - in the example where you call set state one after another:
setState(newValues) // runs validation 1
setState(evenNewerValues) // runs validation 2
// validation 2 completes
// validation 1 completes - user sees 'stale' errors from validation 1
It's not really a hooks vs other implementation approach, but the fact that Formik needs to use a reducer (or even better, state machine) pattern, so that updates can be queued and processed in sequence. Then, in the case of validation (which runs asynchronously), 'stale' validation results need to be ignored, rather updating state.
All that's achievable with hooks I think 🙏
@andycarrell That isn't really the case that I'm describing; I was mainly responding directly to @jaredpalmer's comment about being unable to add a promise or callback that runs after the state commit with hooks. What I'm referring to would enable, in general, running some side effect after an update to formik state completes. like so:
formik.setFieldValue('fieldName', 'newVal').then((updatedFormikState) => {
console.log(updatedFormikState.values.fieldName) //logs 'newVal'
})
You could trigger your async validation by calling it on updatedFormikState from within that returned promise's resolved handler (or an equivalent callback param). To do multiple sequential updates and validations, you'd just chain further updates in additional promises. It is a hooks vs. other implementation issue- that API is not possible with useState.
Stop sending me messages from this email.
All efforts to unsubscribe is futile.
Stop sending me messages from this email.
Thank you 🙏
Sent from my iPhone
On 18 Nov 2020, at 05:32, leo-terratrue [email protected] wrote:
@johnromhttps://github.com/johnrom @jaredpalmerhttps://github.com/jaredpalmer Because hooks are annoying af, there is no way for us to provide a promise or callback after the commit AFAIK. Agreed that this is annoying- making it possible have a callback or promise that runs after setFieldValue's state update is complete would be very useful. But the easy solution to that feels like simply not using hooks for that state in the Formik implementation. Regular class component setState offers the second (callback) parameter, which would enable exactly this. Is there a reason that isn't an option? Adding a callback/promise to APIs like setFieldValue which receives the new formik state as a parameter (implemented internally using class component setState) fees like the easy 80/20 solution to a lot of these kinds of issues. The lack of something like that has been a pretty major pain point for us in using Formik.
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHubhttps://github.com/formium/formik/issues/2083#issuecomment-729399012, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AQORP75NTGQFVT7KNOFRO4LSQNE4JANCNFSM4JTUZFKQ.
I've built a number of large forms in an app using hooks from useFormikContext and only just discovered this validation issue. The solution I've come up with is to add this component to forms that use these hooks:
const Validator = () => {
const {values, validateForm} = useFormikContext()
useEffect(() => {
validateForm(values)
}, [values])
return null
}
It just listens to values and revalidates whenever they change, which has solved this for me.
@tj-mc thank you for the code example. Looks like some form of this approach is what helps address this issue.
I guess what I am wondering if the problem can be solved with 8 lines of code, should not this be integrated into the formik code base?
@gone-skiing No worries. I don't really have a good understanding of the inner workings of Formik, so I'm not sure why this is unsolved as of right now. All I do know is that this issue took ages to find a discussion on and caused me a lot of confusion. Hoping it can be resolved soon, but in the meantime I don't think I'll be using Formik hooks.
Same here - probably about a week of wasted effort for me, it is such a rake. Failure cause is very difficult to track and I can't advocate strongly enough to fix it as soon as possible.
@gone-skiing @tj-mc the solution presented will not make it into the Formik API itself for a variety of reasons. Using an effect, validation does not begin until the render is committed, which could result in validation lag for every project using Formik. Adding it in addition to the current callback-based method results in duplicate validation which would be a performance regression in projects which aren't experiencing this issue. The solution that works for everyone requires a rewrite of the way Formik accesses state internally, which is a complex issue that we'll be targeting for v3.
const Validator = () => { const {values, validateForm} = useFormikContext() useEffect(() => { validateForm(values) }, [values]) return null }
Thought I'd add that my previous hack-fix has not really proved reliable, so don't reach for this as a production-ready solution. Plus as @johnrom pointed out, it's just gonna cause lots of re-validation and re-renders. In my app I managed to get away with not using setFieldTouched anywhere in the form, and the rest of the hooks are behaving as expected.
Btw, Thanks for all your work on this library guys, in all my time using it, this would be the only time it's got in my way.
Btw, Thanks for all your work on this library guys, in all my time using it, this would be the only time it's got in my way.
+1
Any progress on this? I am still having this issue and haven't been able to upgrade from 2.1.4 because of it
The solution that works for everyone requires a rewrite of the way Formik accesses state internally, which is a complex issue that we'll be targeting for v3.
You can follow the state updates here: #3089
Encountered the same issue on [email protected].
If you are using both setFieldTouched() with setFieldValue(), to update touched status during value change. Disablling validation(the 3rd argument) for setFieldTouched() and enabling validation(the 3rd argument) on setFieldValue() could be a workaround.
setFieldTouched(fieldPath, true, false) // do not validate
setFieldValue(fieldPath, newValue, true) // do validate
Disabling validateOnChange so that validation only runs on blur or form submit causes this issue to start popping up a lot. While the shouldValidate arguments of setFieldTouched and setFieldValue work when you want update the touched field in an onChange handler, they aren't the solution in this case where we only want validation to run on blur. The setTimeout approach has worked for me in the meantime, but this is taking advantage of the JavaScript task queue and feels more like a workaround.
Once this issues gets resolved, please do notify...