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>
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()).
https://codesandbox.io/s/2307zv53zy
In the example, when value of <select> changes, the alert dialog does not popup.
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.
| Software | Version(s) |
| ---------------- | ---------- |
| Formik | 1.4.1 |
| React | 16.6.3 |
| Browser | Chrome v71 |
| npm/Yarn | npm 6.4.1 |
| Operating System | Windows |
I confirm this issue, I was bitten by it too, I have a form component for filtering purposes and I dont need button there, hence I submit form on any change. The easiest way to achieve it was to call submitForm like @std4453 in onChange
In my opinion this is a bug in 1.4.1 release, as in 1.4.0 everything worked fine. Probably as the consequence of Remove double rendering on each key stroke. Huge perf boost. as mentioned in 1.4.1 release, so actually maybe this bug was always there, but it was always corrected by chance by second renders which now were removed.
The fix it for now is to wrap submitForm in setTimeout, see https://codesandbox.io/s/1r7yvkkmq4 But this is a hack/workaround, not a fix. It is not Reacty to use setTimeout like this, it should work without it. Plus, we really need a way to easily use Formik with filter kind forms, without submit button, or to have onChange function passed to Form, so we could attach callback to be called on any field change, even without form submitting.
Duplicate of #1209
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.
@klis87 @std4453 submit on change is a very very strange concept to formik. This is because submission will only actually execute if validation passes.
@klis87 I think you are correct, the overrendering was probably hiding this race condition. You were also likely submitting outdated data.
@klis87 the last part will be trivial when hooks arrive with useEffect() or right now component-component
@jaredpalmer thx for examples, we have options to choose from :+1:
Adding a async await on handleChange, fix it.
@jaredpalmer I use Formik submitForm() instead of calling onSubmit() directly because I need the isSubmitting feature of Formik. I still think the problem is that runValidations() should be triggered in the same order as the functions that trigger it.
Strange as calling submitForm() in onChange() might be, there's no where to state it as unrecommended, and confusion arise therefrom. From the user's perspective, a function that is called earlier has cancelled a function called later, this is very hard to understand.
Adding a async await on handleChange, fix it.
wow.
Well handleChange() doc doesn't say that it returns a Promise. Is this intentional?
I have a PR that prevents validation cancellation if triggered by submit. I think a stronger approach though may be to also bail on any further attempts to validate during submit if isSubmitting is true
@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
by instead of onSubmit I mean you could omit onSubmit if u provide just onChange
@klis87 I don't think an additional top-level onChange() is a good idea. The difference between onChange() and validate() is hard to explain.
@klis87 I agree with you. To make sure we are on the same page, the top level onChange will be called only if validation passes yeah? So that's more like a onValidationSuccess? (Not suggesting an alternative term, just for the sake of confirming we are on the same page)
@std4453 If me and @klis87 are on the same page, then it's easy to explain the difference. validate() will run on any keystroke. onChange() will run only if validation passes.
However, I also have a different suggestion. What we want is to submit the form on any change. So is there a reason for not allowing a flag, say, submitOnChange?
@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)
I would like to use submitOnChange functionality here, any idea on when this will be ready?
Adding a async await on handleChange, fix it.
Check: https://codesandbox.io/s/ql8v2l8ll9wow.
Well
handleChange()doc doesn't say that it returns aPromise. 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);
}}
/>
This has been solved in Formik 2. There is now an official auto save example as well.
Commit: https://github.com/jaredpalmer/formik/commit/1fd9580d7da1b5d05f0c4183f038657d6b2b4a02
@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.
@xinkule the preview can be part of the form, you can just use formikProps.values or a <Field> to access the current values of the form. You don't need to submit it to a server or do you?
@xinkule the preview can be part of the form, you can just use formikProps.values or a
<Field>to access the current values of the form. You don't need to submit it to a server or do you?
This is indeed a great idea, I've been using this approach in some of my form components. But the thing is that the preview page is formed by an array of components , and I should not only edit form of each component to reflect the change but also add components to an array by the provided api <FieldArray />. And somehow the add button and the form fields are split in UI, which makes me subscribe to the array in separate places. This way I feel my code become some kind of verbose.
BTW, I've been always struggling through different solutions, sometimes making choices could drive me crazy lol.
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.
@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}
/>
)
}
I will post back I promise, but my button is inside the form. Something in my custom input is preventing this from happening, I think it has something to to with me intercepting on change and calling on change from formik manually
Two issues :Version of @jaredpalmer and @vojtechportes are behaving the same way:
@jaredpalmer I implemented the autosave example, but I am using controlled fields that are being loading by state after mount, this is trigger a save when the data gets loaded into the initial values after mount. How do i stop this initial save?
@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>
}
@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.
Something went wrong
Request failed with status code 404
Most helpful comment
@pupudu exactly, we could call it as
onValidationSuccessas wellre 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
submitOnChangewould also be fine, but probably it would be less flexible, in theory you could do different thing in formonChange/onValidationSuccessandonSubmit(clicking submit button vs just changing some field without explicit submit)