Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v2] Validation runs on old values after setFieldTouched #2083

Open
bkiac opened this issue Dec 2, 2019 · 42 comments
Open

[v2] Validation runs on old values after setFieldTouched #2083

bkiac opened this issue Dec 2, 2019 · 42 comments

Comments

@bkiac
Copy link

bkiac commented Dec 2, 2019

🐛 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
@kirjai
Copy link

kirjai commented Jan 6, 2020

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

@jaredpalmer
Copy link
Owner

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.

@jaredpalmer
Copy link
Owner

My point above is that this isn’t a bug, it’s how React works.

@bkiac
Copy link
Author

bkiac commented Jan 7, 2020

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?

@straiki
Copy link

straiki commented Jan 7, 2020

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

@arianitu
Copy link

arianitu commented Jan 16, 2020

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 and then it finally validates. But why is it not validating when I select an option?

@arianitu
Copy link

arianitu commented Jan 16, 2020

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

@arianitu
Copy link

arianitu commented Jan 17, 2020

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:

#2046

@sebastian-ludwig
Copy link

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

@pcwa-ahendricks
Copy link

pcwa-ahendricks commented Jan 23, 2020

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

@marek-sed
Copy link

marek-sed commented Mar 6, 2020

If you have that option you can call setFieldTouched from onBlur handler.
It did fix this issue with react-select

@stale stale bot added the stale label May 6, 2020
@vicasas
Copy link

vicasas commented Jun 6, 2020

you can use the following, it worked for me

setTimeout(() => setFieldTouched(value, true))

Wrap setFieldTouched in a setTimeout() function

@stale stale bot removed the stale label Jun 6, 2020
@nceeoh7
Copy link

nceeoh7 commented Jul 25, 2020

The setTimeout method fixed this issue for me with my React-Select fields.

@wallace-sf
Copy link

wallace-sf commented Sep 17, 2020

you can use the following, it worked for me

setTimeout(() => setFieldTouched(value, true))

Wrap setFieldTouched in a setTimeout() 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]);

@simofacc
Copy link

simofacc commented Oct 6, 2020

@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
Copy link

wallace-sf commented Oct 6, 2020

@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>
    </>
  );
});

@gone-skiing
Copy link

gone-skiing commented Nov 17, 2020

@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.

@johnrom
Copy link
Collaborator

johnrom commented Nov 17, 2020

@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: #2846 (comment)

@gone-skiing
Copy link

@johnrom thanks for the comment. It is great to see that the team is thinking about it.

@leo-terratrue
Copy link

leo-terratrue commented Nov 18, 2020

@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.

@andycarrell
Copy link

andycarrell commented Nov 18, 2020

@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 🙏

@leo-terratrue
Copy link

leo-terratrue commented Nov 18, 2020

@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.

@syke209
Copy link

syke209 commented Nov 18, 2020 via email

@tj-mc
Copy link

tj-mc commented Nov 27, 2020

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.

@gone-skiing
Copy link

@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?

@tj-mc
Copy link

tj-mc commented Nov 28, 2020

@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.

@gone-skiing
Copy link

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.

@johnrom
Copy link
Collaborator

johnrom commented Nov 30, 2020

@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.

@tj-mc
Copy link

tj-mc commented Dec 8, 2020

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.

@gone-skiing
Copy link

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

@Aerophite
Copy link

Any progress on this? I am still having this issue and haven't been able to upgrade from 2.1.4 because of it

@johnrom
Copy link
Collaborator

johnrom commented Mar 25, 2021

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

@frantic1048
Copy link
Contributor

Encountered the same issue on formik@2.1.7.

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

@mcsimps2
Copy link

mcsimps2 commented May 3, 2022

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.

@AkshayGadekar
Copy link

Once this issues gets resolved, please do notify...

@cheapsteak
Copy link

looks like there might be a workaround in using setValues instead of setFieldValue?

setValues((fields) => ({...fields, someField: newValue}, true))

guessing because that's no longer running sync?

@blentz100
Copy link

Thank you @vicasas - that solution worked for me. I'm running React 18.2 and Formik 2.2.9

you can use the following, it worked for me

setTimeout(() => setFieldTouched(value, true))

Wrap setFieldTouched in a setTimeout() function

@oussidr
Copy link

oussidr commented May 30, 2023

this solution will work also :

setFieldValue("value", []).then(() => { setFieldTouched("value", true) })

@mcsimps2
Copy link

mcsimps2 commented Aug 1, 2023

FYI, the setTimeout(() => setFieldTouched(value, true)) workaround doesn't work in Mobile Safari, but adding in a delay alleviates the issue, e.g. setTimeout(() => setFieldTouched(value, true), 100).

On Chrome, the workaround works flawlessly by letting the values get up to date before running validation.

@oussidr The Promise syntax with the latest version of formik still exhibits the issue, unfortunately.

@Jestermaxrko
Copy link

Jestermaxrko commented Jan 25, 2024

After investigating formik source code I found intresting line of code

const state = stateRef.current;

ref value is set to new variable and then used everywhere, because of that when stateRef.current is updated here state variable still has old reference(until next render) which is used for validation in setFieldTouched method here

Replacing state.values with stateRef.current.values garantees that validation will always run on actual values. I submitted PR with this change.

@mcsimps2
Copy link

@Jestermaxrko I've been using your fork, it seems to work well. Now I can remove all these setTimeout statements... Thanks so much, hoping it gets merged in and released!

@JanderSilv
Copy link

Any predictions if this fix will be released in versions 2.x or just 3.x?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests