Formik: Validation runs on old value when onChange is called within onBlur

Created on 11 Dec 2019  路  3Comments  路  Source: formium/formik

馃悰 Bug report

Current Behavior

I have a complex component that reformats the user input when they blur the component. When using <Field component={Component}/> the validation runs on the previous value.

Expected behavior

Validation should run on the state after the onBlur.

Reproducible example

https://codesandbox.io/s/formik-codesandbox-template-1ewgq
2019-12-11 11 52 20

Suggested solution(s)

N/A

Additional context

Related #2083, #1977, #2025

Your environment

| Software | Version(s) |
| ---------------- | ---------- |
| Formik | 2.0.7
| React |16.12.0
| TypeScript | N/A
| Browser | Chrome 78.0.3904.108
| npm/Yarn | N/A
| Operating System | MacOS 10.14.6

FastField Field Formik Medium Feature Request

Most helpful comment

The solution _should_ be to use setFieldValue and setFieldTouched which let you override validation.

So you should be able to write:

const Component = ({ field, form, ...rest }) => {
  const onBlur = e => {
    e.target.value = "Blurred Value";
    form.setFieldValue(field.name, "Blurred Value", true /* force validation */);
    form.setFieldTouched(field.name, true, false /* force abort validation */);
  };

  return (
    <input
      style={{ border: "2px solid red" }}
      {...field}
      onBlur={onBlur}
      {...rest}
    />
  );
};

However, because setFieldValue requires validateOnChange and shouldValidate to run validation, you can't in this situation.

https://github.com/jaredpalmer/formik/blob/master/packages/formik/src/Formik.tsx#L545

The current workaround would be to keep validateOnChange to true, and then override both onChange and onBlur in your component using setFieldValue (disabling validation in onChange, but keeping it in the onBlur). Not great tbh.

I think a solution IMHO is to alter the shouldValidate default from true, to just be the value of the validateOnChange prop (which defaults to true as well). AFAICT this wouldn't be a breaking change, but would support my code above. This would also make it easier to do this kind of parse/formatOnBlur in your example.

Yet again, not having 2nd argument callback to setState (ie. the old inline cDU) is by far the most annoying parts of hooks. It's the cause of so much pain. It would be so much nicer if setXXXXX resolved after the update or had a callback that would run after the commit, but unfortunately this isn't possible with hooks.

All 3 comments

The solution _should_ be to use setFieldValue and setFieldTouched which let you override validation.

So you should be able to write:

const Component = ({ field, form, ...rest }) => {
  const onBlur = e => {
    e.target.value = "Blurred Value";
    form.setFieldValue(field.name, "Blurred Value", true /* force validation */);
    form.setFieldTouched(field.name, true, false /* force abort validation */);
  };

  return (
    <input
      style={{ border: "2px solid red" }}
      {...field}
      onBlur={onBlur}
      {...rest}
    />
  );
};

However, because setFieldValue requires validateOnChange and shouldValidate to run validation, you can't in this situation.

https://github.com/jaredpalmer/formik/blob/master/packages/formik/src/Formik.tsx#L545

The current workaround would be to keep validateOnChange to true, and then override both onChange and onBlur in your component using setFieldValue (disabling validation in onChange, but keeping it in the onBlur). Not great tbh.

I think a solution IMHO is to alter the shouldValidate default from true, to just be the value of the validateOnChange prop (which defaults to true as well). AFAICT this wouldn't be a breaking change, but would support my code above. This would also make it easier to do this kind of parse/formatOnBlur in your example.

Yet again, not having 2nd argument callback to setState (ie. the old inline cDU) is by far the most annoying parts of hooks. It's the cause of so much pain. It would be so much nicer if setXXXXX resolved after the update or had a callback that would run after the commit, but unfortunately this isn't possible with hooks.

Thanks for the reply @jaredpalmer

I think a solution IMHO is to alter the shouldValidate default from true, to just be the value of the validateOnChange prop (which defaults to true as well). AFAICT this wouldn't be a breaking change, but would support my code above.

I don't think that will work for the example that I gave where validateOnChange={false} because
setFieldValue wouldn't trigger validation and setFieldTouched would still be looking at stale data.

Changing it so shouldValidate takes precedence over the validateOnChange prop would help with the workaround that you provided.

// before
return validateOnChange && shouldValidate
        ? validateFormWithLowPriority(setIn(state.values, field, value))
        : Promise.resolve();
// after
const willValidate = shouldValidate === undefined ? validateOnChange : shouldValidate;
return willValidate
        ? validateFormWithLowPriority(setIn(state.values, field, value))
        : Promise.resolve();

Regarding the setState lack of callback. Could we introduce a provider that uses the older setState syntax? e.g. a <StateProvider> that gives us state and dispatch where dispatch calls the reducer and returns a promise that resolves when the setState callback is called.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

green-pickle picture green-pickle  路  3Comments

jeffbski picture jeffbski  路  3Comments

ancashoria picture ancashoria  路  3Comments

emartini picture emartini  路  3Comments

outaTiME picture outaTiME  路  3Comments