jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
34.01k stars 2.79k forks source link

Promise returned from submitForm not rejected when form is invalid #1580

Open Aaronius opened 5 years ago

Aaronius commented 5 years ago

🐛 Bug report

Current Behavior

When calling submitForm on an invalid form, the returned promise is resolved.

Expected behavior

When calling submitForm on an invalid form, the returned promise should be rejected according to the documentation.

Reproducible example

https://codesandbox.io/s/formik-codesandbox-template-81kio?fontsize=14

Suggested solution(s)

Reject the promise when there are errors. It seems the related code is found here: https://github.com/jaredpalmer/formik/blob/22a17b84700af503828ca27c0d2e6a283112c4ea/src/Formik.tsx#L439-L448

Additional context

Your environment

Software Version(s)
Formik 1.5.7
React 16.8.6
TypeScript n/a
Browser Chrome 74
npm/Yarn 6.4.1
Operating System Mac Mojave
jaredpalmer commented 5 years ago

Did I write that in the docs? On mobile and can’t git blame.

Aaronius commented 5 years ago

It seems to have been written by @FBerthelot and merged by you.

Commit: https://github.com/jaredpalmer/formik/commit/4dbb93f21ee120d2e30e3707e9c3d3380c4c4c1b PR: https://github.com/jaredpalmer/formik/pull/1423

It looks like after the PR was merged, there was discussion around this problem. FWIW, I'd rather the promise actually get rejected than just fixing the documentation to match the code.

justindmyers commented 5 years ago

Running into this problem myself, but instead it seems submitForm doesn't return a correct promise at all. If you look at the submitForm function, it only returns the validation promise, it doesn't return anything from executeSubmit, so the promise will always return undefined no matter what.

https://github.com/jaredpalmer/formik/blob/914ccde516e36e40bcc21996bb1b95372b95689b/src/Formik.tsx#L443-L445

To fix it, I just copied the code out into my own submitForm function and returned this.props.onSubmit and now it works just fine. executeSubmit seems useless since it just calls this.props.onSubmit but isn't used anywhere else.

clflowers5 commented 5 years ago

Spent a few hours digging around this myself tonight. Didn't realize the docs were wrong till I pulled down the source. The current documentation for 1.5.8 shows

submitForm: () => Promise
Trigger a form submission. The promise will be rejected if form is invalid.

But like justindmyers stated above, submitForm will always resolve with undefined.

jaredpalmer commented 5 years ago

This is fixed in 2.x

latuszek commented 5 years ago

Can you fix this also for 1.5.x? 2.x is still in pre-release phase.

devinsm commented 5 years ago

@jaredpalmer I'm using 2.0.1-rc.13 and this appears to still be broken: https://github.com/devinsm/formik-submitform-bug

devinsm commented 5 years ago

The work around for now is to wrap submitForm in custom logic:

// submitForm and validateForm are Formik's submitForm and validateForm
function fixedSubmitForm({ submitForm, validateForm }) {
  return new Promise((resolve, reject) => {
    submitForm()
      .then(validateForm)
      .then(errors => {
        const isValid = Object.keys(errors).length === 0;
        if (isValid) {
          resolve();
        } else {
          reject();
        }
      })
      .catch(e => {
        console.log('error: ', e);
        reject();
      });
  });
}

Of course this causes the form to be validated twice. In some situations this may lead to unacceptable lag after the user hits submit, but for smaller forms with synchronous validation it should work fine.

josephsiefers commented 5 years ago

Interestingly, the typing is also inconsistent with the documentation. In 1.5.8 (latest release at time of writing), dist/types.d.ts has

export interface FormikActions<Values> {
  ...
  submitForm(): void;
  ...
}
jwmann commented 4 years ago

Thank you @devinsm this is a useful workaround for the time being

@josephsiefers I can verify this to be the case as well

oowowaee commented 4 years ago

Is this fixed? Using 2.1.1 I also ran into an issue where calling SubmitForm, the returned promise is resolved even though the form is invalid.

HannahG0703 commented 4 years ago

Is this fixed? Using 2.1.1 I also ran into an issue where calling SubmitForm, the returned promise is resolved even though the form is invalid.

Same.

sebastianpatten commented 4 years ago

Looks like 2.1.2 is not rejecting when the form is invalid.

dorklord23 commented 4 years ago

I second @sebastianpatten. The fix doesn't work even in v2.1.2

hristof commented 4 years ago

Not working in v2.1.3 too.

jimmyn commented 4 years ago

Seems in 2.1.4 it is still an issue

anaelChardan commented 4 years ago

Still got the problem too.

oefimov commented 4 years ago

TWIMC fixed with #1904 then reverted with #1198 2.0.7 is the only version with expected behaviour

MrNghia123 commented 4 years ago

Would love to have this fixed. 2.1.4.

fabb commented 4 years ago

I guess this was reverted because <form onSubmit={formProps.handleSubmit} would print a console error when clients do not catch the thrown error.

We need the functionality too in order to send analytics information and other side effects on failed form validation when the user tried to submit. Currently we use this workaround: https://github.com/jaredpalmer/formik/pull/2103#issuecomment-574565897, but it is not very nice, and also it might break when React Concurrent Mode lands, and Automatic batching of multiple setStates happens (which even happens in Blocking Mode).

I would suggest to add an additional config parameter to FormikConfig: onSubmitCancelledByFailingValidation.

    const formProps = useFormik({
        initialValues: myInitialValues,
        validate: myValidateFunction,
        onSubmit: mySendForm, // is only executed when validation succeeds
        onSubmitCancelledByFailingValidation: myFallbackOnFailedValidation,
    })

This would be backwards-compatible, and also it would separate the general validation (that can happen when e.g. just typing in an input, or blurring a field) from validation that is triggered by trying to send the form.

An alternate solution idea would be to add a second parameter to the validate config parameter to signal that the validation is being executed because the user is trying to send the form: validate?: (values: Values, validationTrigger: 'field-changed' | 'field-blurred' | 'submit') => void | object | Promise<FormikErrors<Values>>

    const formProps = useFormik({
        initialValues: myInitialValues,
        validate: (values, validationTrigger) => {
            const result = myValidateFunction()
            if (validationTrigger === 'submit') {
                myFallbackOnFailedValidation()
            }
            return result
        },
        onSubmit: mySendForm, // is only executed when validation succeeds
    })

This would also be backwards-compatible, but not as elegant on the client side, and also would not work for a Yup validationSchema if used. Therefore I would prefer the first approach.

@jaredpalmer What do you think about these suggestions? Would you accept a PR?

why06 commented 4 years ago

This is still an issue, almost been a year. Is there any traction on resolving this?

fabb commented 4 years ago

I created a PR with my solution suggestion, constructive discussion about the approach welcome: #2485.

rmincling commented 4 years ago

Is there any update with this? I've had to revert back to 2.0.7 in order to get access to the data returned from submitForm Promise.

alistairholt commented 4 years ago

Doesn't seem to be rejecting when the form is invalid in 2.1.5 either 😞 .

danawoodman commented 4 years ago

I hate to do it, but.... bump!

unbugx commented 4 years ago

2.2.5 bug is still there (

unbugx commented 4 years ago

Probably it is off topic, I describe my case

export const InfoForm = () => {
  .................
  .................

  const formik = useMemo(() => ({
    initialValues,
    onSubmit: async (values:, actions) => {
      .....
    },
    validate: (values: typeof initialValues) => {
      const errors = {};

      if (!values.reason) {
        errors.reason = ....;
      }

      return errors;
    },
  }), []);

  return (
    <>
      <Formik {...formik}>
        <Form>
          ......
          ......
         <Submit className={s.submit}>Submit</Submit>
        </Form>
      </Formik>
    </>
  );
};

If any error happens in onSubmit or in validate function then I just see warning An unhandled error was caught from submitForm() So I can't use Sentry here :(.

What helped me:

I extracted submit button and start use submitForm on click:

export const Submit = ({
  children,
  ...restProps
}) => {
  const { isSubmitting, submitForm } = useFormikContext();

  return (
    <ButtonPrimary
      type='submit'
      loading={isSubmitting}
      {...restProps}
      onClick={submitForm}
    >
      {children}
    </ButtonPrimary>
  );
};

So now I can use Sentry without any additional magic, though it is also magic :)

Снимок экрана 2020-11-23 в 18 15 18
Macilias commented 3 years ago

Any news?

// submitForm and validateForm are Formik's submitForm and validateForm function fixedSubmitForm({ submitForm, validateForm }) { ...

was till now the only way to submit a form from an button, but it also started to ignore validation ;(

maciejtoporowicz commented 3 years ago

I used this as a workaround:

<Button onClick={() => {
  formik
    .submitForm()
    .then(() => {
      if (formik.isValid) {
        handleFormSuccessfullySent();
      } else {
        handleFailedValidationOnFormSubmission();
      }
    })
}} />

I also had validateOnMount set to true, otherwise formik.isValid condition was true when the form wasn't touched before trying to submit it:

  const formik = useFormik({
    // ...
    validateOnMount: true
  });
callumjg commented 3 years ago

If you just need to reject the promise returned by the submitForm function when the form is invalid, this is an easy workaround:

const submit = () => helpers.submitForm().then(() => helpers.isValid || Promise.reject())
<SubmitButton onClick={submit}>Submit</SubmitButton>

I also needed to use @maciejtoporowicz's suggestion to add thevalidateOnMount flag.

fabb commented 3 years ago

I don‘t understand why validateOnMount is necessary, submitForm should trigger validation in any case, what am I missing?

khanilov commented 3 years ago

@callumjg Doesn't you use stale helpers object from previous render in your then handler?

callumjg commented 3 years ago

@callumjg Doesn't you use stale helpers object from previous render in your then handler?

I haven't come across that problem. I'm pretty sure that the submit function gets reevaluated every render with the lastest helpers object (at least in my implementation). Only the initial render seemed to be a problem, but this was solved by using validateOnMount.

johnrom commented 3 years ago

@callumjg what khanilov means is that if there is an update to isValid between the time this last render was committed and then(), it will not be reflected in helpers.isValid as the helpers object is not modified in the submit function.

Generally isValid isn't changed during this time if you happen to be using synchronous validation and validating onBlur or onChange, because validation would have already occurred at that time for a given set of values, which could explain why you may not be experiencing this issue.

In v3 you will be able to use formik.getState().isValid to get the latest value.

callumjg commented 3 years ago

... Generally isValid isn't changed during this time if you happen to be using synchronous validation and validating onBlur or onChange, because validation would have already occurred at that time for a given set of values, which could explain why you may not be experiencing this issue. ...

Thanks for clarifying. Yes, that describes my use case.

ghost commented 2 years ago

If you just need to reject the promise returned by the submitForm function when the form is invalid, this is an easy workaround:

const submit = () => helpers.submitForm().then(() => helpers.isValid || Promise.reject())
<SubmitButton onClick={submit}>Submit</SubmitButton>

I also needed to use @maciejtoporowicz's suggestion to add thevalidateOnMount flag.

This just basically solved the problem, thank you so much 🥂

raegen commented 1 year ago

Yo, today's Sunday, August the 20th, year 2023, exactly 4 years, 2 months and 17 days since this issue report was created.

Qanpi commented 1 year ago

How is it possible that this is unresolved to this day? What's the point of using Formik if it can't handle validation for you?

evgkord commented 1 year ago

Still waiting for fixes


Found temporal solution in doing validateForm() again and check for errors:

formik.submitForm().then(() => {
        return formik.validateForm()
            .then((errors) => {
                return _.isEmpty(errors) ? Promise.resolve() : Promise.reject();
            });
    }).then(() => {
        // actions on success
    }).catch(() => {
        // actions on error
    });
Vekeryk commented 7 months ago

It is still not fixed for 2.4.5