formik: How to trigger form submit on change

🐛 Bug report

Current Behavior

In our project we’ve created an MUI Select component and want it to do something every time its value has changed.

We’ve chosen to wrap it inside a <Formik> tag in order to avoid dealing with events, and to keep code familiar to other members of the group. (since we use Formik in a great many places of the project)

Currently we write things like this: (details are omitted)

<Formik
    initialValues={{ value: initialValue }}
    onSubmit={({ value }, formikBag) => onSubmit(value, formikBag)}
>{({ values, handleChange, submitForm }) => (
        <Select
            name="value"
            value={values.value}
            onChange={(e) => {
                handleChange(e);
                submitForm();
            }}
        />
    )}
</Formik>

Expected behavior

Before Formik 1.4.0, everything works as fine.

When users click on the Select component, its onChange() handler got triggered, which calls handleChange() first (and sets value) then submitForm() (and triggers onSubmit()).

Reproducible example

https://codesandbox.io/s/2307zv53zy

In the example, when value of <select> changes, the alert dialog does not popup.

Suggested solution(s)

One available solution is to set validateOnChange to false on the Formik component.

Have dug into Formik code (v1.4.1) for a bit, I found the cause of this problem.

Both handleChange() and submitForm() call Formik._runValidations(), which starts a Promise to run validations. Since v1.4.0 introduced cancellable Promises, the Promise started later will cancel the one started earlier.

I expected the Promise created by submitForm() to be started after the one created by handleChange(), however handleChange() actually starts the Promise in the callback of React Component.setState(), which is deferred by React to the next frame and therefore started later than the one created by submitForm() and, boom.

I don’t know whether what I’ve done is recommended by or even should be done with Formik, or, whether this will be considered a bug. If not, should this behavior be documented? It was pretty confusing to me at the beginning.

In case of any suggestions, please let me know. Thanks.

Your environment

Software Version(s)
Formik 1.4.1
React 16.6.3
Browser Chrome v71
npm/Yarn npm 6.4.1
Operating System Windows

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 29
  • Comments: 34 (11 by maintainers)

Most upvoted comments

@pupudu exactly, we could call it as onValidationSuccess as well

re difference with validate, also I would add that validate is for validation, so I cannot see how it could be hard to explain the difference in docs

again, forms without submit buttons are quite a common use case, especially for filter kind of forms, form library imho should support this use case with clean code too, especially this is just 1 extra prop easy to explain

submitOnChange would also be fine, but probably it would be less flexible, in theory you could do different thing in form onChange/onValidationSuccess and onSubmit (clicking submit button vs just changing some field without explicit submit)

Adding a async await on handleChange, fix it. Check: https://codesandbox.io/s/ql8v2l8ll9

wow.

Well handleChange() doc doesn’t say that it returns a Promise. Is this intentional?

setTimeout could get it works too. The magic is to let it run on the next cycle tick.

<Select
    name="value"
    value={values.value}
    onChange={(e) => {
        handleChange(e);
        setTimeout(submitForm, 0);
    }}
/>

@jaredpalmer would you consider adding onChange to top level formik component, instead of or next to onSubmit? this callback would be called anytime a value is changed, provided the form is valid, then we wouldnt need to add hacks like this to submit the form on each key stroke

Adding a async await on handleChange, fix it.

Check: https://codesandbox.io/s/ql8v2l8ll9

@JaredDahlke sounds complicated. Provide a codesandbox and I’ll take a look. A quick test against initialValues should help you.

const MyAutoSavingComponent = () => {
  const formik = useFormikContext();

  useEffect(() => {
    // use your own equality test or react-fast-compare because they are probably different objects
    if (formik.values !== formik.initialValues) {
      formik.submitForm(); // or onSubmit if you want to do validations before submitting
    }
  }, [formik.values]); 
  // not listening for initialValues, because even if they are updated you probably don't want to autosave.
  return null;
}

const MyForm = () => {
  const [initialValues, setInitialValues] = React.useState({ firstName: '', lastName: '' });
  useEffect(() => setInitialValues({
      firstName: 'john',
      lastName: 'rom',
  }), []);
  return <Formik 
    initialValues={initialValues}
  >
    <Form>
      <Field name="firstName" />
      <Field name="lastName" />
      <MyAutoSavingComponent />
    </Form>
  </Formik>
}

@kelly-tock I guess you could do something like this

import React, { useEffect } from 'react'
import { Form, Field, Formik, FormikProps } from 'formik'

/* AutoSubmit component */

interface AutoSubmitProps {
  values: any
  submitForm: () => void
}

const AutoSubmit: Reac.FC<AutoSubmitProps> = ({ values, submitForm }) => {
  useEffect(() => {
    submitForm()
  }, [values, submitForm])

  return null
}

/* MyForm component */

const initialValues: FormValues = {
  test: ''
}

interface FormValues {
  test: string
}

interface MyFormProps {
  onSubmit: () => void
}

const MyForm: React.FC<MyFormProps> = ({ onSubmit }) => {
  const renderForm = ({
    isSubmitting,
    values,
    submitForm,
  }: FormikProps<FormValues>) => {
   
    return (
      <>
        <Form>
          <Field
            name="test"
          />
        </Form>
        <AutoSubmit values={values} submitForm={submitForm} />
      </>
    )
  }

  return (
    <Formik<FormValues>
      onSubmit={onSubmit}
      render={renderForm}
      initialValues={initialValues}
    />
  )
}

@johnrom

I implemented this and it worked perfectly. Now the problem is my API is having a hard time handling all of the patch requests I’m sending to it lol.

I ended up implementing this version based off of the doc’s example. I also added dirty to your equality test:

const AutoSave = ({ debounceMs }) => {
	const formik = useFormikContext()
	const debouncedSubmit = React.useCallback(
		debounce(() => formik.submitForm().then(() => console.log('saved')), debounceMs),
		[debounceMs, formik.submitForm]
	)

	React.useEffect(() => {
		if (formik.values !== formik.initialValues && formik.dirty)
			debouncedSubmit()
	}, [debouncedSubmit, formik.values])

	return null
}

And my withFormik object looks like this:

const FormikForm = withFormik({
	mapPropsToValues: (props) => {
		let profileName = ''  //initial vals
		let websiteUrl = ''  //initial vals
		let twitterProfileUrl = ''  //initial vals
		let industryVerticalId = ''  //initial vals
		let competitors = []  //initial vals
		if (props.basicInfo.brandName.length > 0) {
			profileName = props.basicInfo.brandName //***reinitialize the form with any redux changes
		}
		if (props.basicInfo.websiteUrl.length > 0) {
			websiteUrl = props.basicInfo.websiteUrl ////***reinitialize the form with any redux changes
		}
		if (props.basicInfo.twitterProfileUrl.length > 0) {
			twitterProfileUrl = props.basicInfo.twitterProfileUrl //***reinitialize the form with any redux changes
		}
		if (!isNaN(props.basicInfo.industryVerticalId)) {
			industryVerticalId = props.basicInfo.industryVerticalId. //***reinitialize the form with any redux changes
		}
		if (props.competitors.length > 0) {
			competitors = props.competitors //***reinitialize the form with any redux changes
		}

		return {
			brandProfileId: props.basicInfo.brandProfileId,
			accountId: props.currentAccountId,
			basicInfoProfileName: profileName,
			basicInfoWebsiteUrl: websiteUrl,
			basicInfoTwitterProfile: twitterProfileUrl,
			basicInfoIndustryVerticalId: industryVerticalId,
			topCompetitors: competitors,
			topics: props.topics,
			scenarios: props.scenarios,
			categories: props.categories
		}
	},
	handleSubmit: (values, { props, setSubmitting }) => {

		let brandProfile = {
			brandProfileId: values.brandProfileId,
			accountId: values.accountId,
			brandName: values.basicInfoProfileName,
			websiteUrl: values.basicInfoWebsiteUrl,
			industryVerticalId: values.basicInfoIndustryVerticalId,
			twitterProfileUrl: values.basicInfoTwitterProfile,
			topics: values.topics,
			competitors: values.topCompetitors,
			scenarios: values.scenarios,
			categories: values.categories
		}

		props.saveBrandProfile(brandProfile) // this is a redux action that calls API
	
	},
	enableReinitialize: true,
	validateOnMount: true,
	validationSchema: schemaValidation
})(CreateBrandProfile)

export default connect(mapStateToProps, mapDispatchToProps)(FormikForm)

Instead of showing the AutoSave verbiage from the example I’m going to just render a custom loader based off of some redux state. I know this is messy but I’m still learning! Really starting to see the power of the library, but it has had a pretty rough learning curve for me.

@jaredpalmer I come up with a scenario where trigger submit onChange is really necessary.

Say I have a page editor, left side is a preview of the page, right side is a form, when I type some kind of config on the right side, the preview will show content immediately.

And I shouldn’t use a submit button because it would really break user experience when you have to click a button every time you want to see the change.

Do you have any suggestions for this kind of situation ? Thanks.

https://codesandbox.io/s/ql8v2l8ll9

Something went wrong Request failed with status code 404

@klis87 I don’t think an additional top-level onChange() is a good idea. The difference between onChange() and validate() is hard to explain.

Adding a async await on handleChange, fix it.

Check: https://codesandbox.io/s/ql8v2l8ll9

wow.

Well handleChange() doc doesn’t say that it returns a Promise. Is this intentional?

I guess my question is why you need to validate through formik submission on every keystroke. Can you just call your submit function directly from your handler and avoid validation altogether?

If you really really want to do this, just note that your onSubmit function will still not run if there are errors returned by validation. Regardless, here my suggested solution: https://codesandbox.io/s/m4mzpn166j.

For those who are wondering how to handle this with useFormik, I used as following.

const {
    handleSubmit,
    handleChange,
    values,
    initialValues
  } = useFormik({
    initialValues: {
      ...
      ...
    },
    validate: (values) => validateForm(values),
    onSubmit: (values) => submitForm(values)
});

useEffect(() => {
  if (values !== initialValues) {
    submitForm(values);
  }
}, [values]);

Two issues :Version of @jaredpalmer and @vojtechportes are behaving the same way:

  1. autosubmitting on mount or dirty (should submit only if the form is valid),
  2. the field is loosing focus, ie if Iam typing in a textarea … after a few letters, I am loosing the focus

is there an example of this that is not formik 2.0? I basically an keeping a form with no submit button in sync the hard way… onValidateSuccess sounds great to me.