Hi @jaredpalmer! I was looking to migrate my forms to a simpler library and wondering if the Formik's API would support auto-saving forms. I went through the README, but wasn't sure if it would be possible (Apologies if I missed something!)
Usecase:
Creating an auto-saving form, where values are saved as the user enters them. This, in most cases, will imply that the changes are saved (and/or validated server-side) onChange, though probably debounced. In some cases, onBlur can work but it is not always the optimum user-experience (for example, choosing a username).
Users might choose to submit in one of two manners - submit the entire form, or submit just that field. This is likely to happen when it is a heavier or a longer form.
So, the two features needed would be -
Thank you!
PS. FWIW, I hacked together a solution currently using Redux-Form and React-Debounce-Input that can be found here - http://gist.github.com/oyeanuj/2f69ebe1004e2ff47d7550d957bf05cf with the basic overview in the file TheAutoSavingForm.md in that gist.
Would also love to see this is I'm having to implement this as an external HOC ATM. What about having handleChange and handleBlur in options? I haven't really looked at the internals so not sure how feasible this is but I think an API like that would allow us to easily implement things like auto-saving forms according to our own specifications
A hyper-simple approach to this I'm using for some toggle switch only forms:
const withAutoSaving = Component => props => {
const handleChangeAndSubmit = e => {
props.handleChange(e);
setImmediate(() => props.handleSubmit(e));
};
return <Component {...props} handleChange={handleChangeAndSubmit} />;
};
Place it after withFormik in a HOC composition and it works. Could probably add some sort of validations to it through yup as well. Would love to see first class API support for these needs tho 馃檭
@slightlytyler I would suggest handling this outside change and inside of componentWillReceiveProps (and debounce it). You could also hook into context just like formik-persist does.
Or do this...
import React from 'react';
import debounce from 'lodash.debounce' // or whatevs
import isEqual from 'lodash.isEqual'
const withAutoSave = Component => {
return class SaveFormik extends React.Component {
state = {
isCommitting: false,
commitError: null
}
componentWillReceiveProps(nextProps) {
if (!isEqual(nextProps.values, this.props.values)) {
this.commit()
}
}
commit = debounce(() => {
// do whatever you want to to save stuff
this.props.handleSubmit({ preventDefault: () => {} })
// or
// imagine handleCommit has to return a promise?
this.setState({ isCommitting: true })
this.props.handleCommit(this.props.values)
.then(
() => this.setState({ isCommitting: false }),
() => this.setState({ isCommitting: false, commitError })
)
}), 300)
}
render() {
return (
<Component {...this.props} {...this.state} />
);
}
}
}
export default AutoSaveFormik;
@jaredpalmer @slightlytyler thanks for the ideas!
To have effective auto-saving forms, one'd often need to just submit the field that changed, and not the entire form. So, would this be an HoC to the entire form (and user needing to implement logic to detect what changed) or HoC to each element in the form?
@jaredpalmer Do you think this should be part of the Formik API - atleast a general enough wrapper that users need not repeat individually?
The goal of my example to show the pattern you might use to handle pretty much kind of extended Formik behavior. Much like React Router 4, Formik is just a React component. This means that the problems you run into can usually be solved with just React and don鈥檛 require expanding the API. That being said, if there is a community need, I鈥檇 probably codify and bless a solution by creating formik-autosave
Gonna close this down.
jaredpalmer's Formik-Autosave.jsx:
https://gist.github.com/jaredpalmer/56e10cabe839747b84b81410839829be
Taking inspiration from formik-persist, I wrote this:
type SubmitOnChangeProps = { debounce: Number, onSubmit: Function };
class _SubmitOnChange extends React.Component<
SubmitOnChangeProps & { formik: FormikProps<any> },
{}
> {
static defaultProps = {
debounce: 300,
};
changed = debounce((props: FormikProps<{}>) => {
this.props.onSubmit(props);
}, this.props.debounce);
componentDidUpdate(
prevProps: SubmitOnChangeProps & { formik: FormikProps<any> }
) {
if (!isEqual(prevProps.formik.values, this.props.formik.values)) {
this.changed(this.props.formik);
}
}
render() {
return null;
}
}
export const SubmitOnChange = connect(_SubmitOnChange);
TypeScript sample code
import _ from "lodash";
import React from "react";
import { InjectedFormikProps } from "formik";
type FormikComponent<Props, Values> = React.ComponentType<InjectedFormikProps<Props, Values>>;
type HOC = <Props, Values>(Component: FormikComponent<Props, Values>) => FormikComponent<Props, Values>;
const withAutoCommit: HOC = (Component) => (props) => {
React.useEffect(_.debounce(props.submitForm, 300), [props.values]);
return React.createElement(Component, props);
};
export default withAutoCommit;
Here is what I'm doing:
import { useRef, useEffect, useState } from 'react'
import { connect } from 'formik'
import isEqual from 'lodash.isequal'
const usePrevious = value => {
const ref = useRef()
useEffect(() => { ref.current = value })
return ref.current
}
const AutoSave = ({ formik: { values }, onSave, render }) => {
const previousValues = usePrevious(values)
const [saving, setSaving] = useState(false)
function callback(value) {
setSaving(false)
return value
}
function save() {
if (previousValues && Object.keys(previousValues).length && !isEqual(previousValues, values)) {
setSaving(true)
onSave(values).then(callback, callback)
}
}
useEffect(() => { save() }, [values])
return render({ isSaving })
}
export default connect(AutoSave)
But I also tweak my input fields, so they only trigger "onChange" event on blur. Thus, no need for debounce.
export const InputField = (props) => <Field component={InputComponent} {...props} />
function InputComponent({ field, form: { handleChange, handleBlur }, ...props }) {
const onBlur = e => { handleChange(e); handleBlur(e) }
return <input {...field} onChange={null} onBlur={onBlur} {...props} />
}
And finally:
import React from 'react';
import { Formik } from 'formik'
import { InputField } from '...'
import AutoSave from '...'
import apiCall from '...'
const onSaving = values => apiCall(values) /* must return a promise */
.then(...)
.catch(...)
const App = () => (
<div>
<h1>My form</h1>
<Formik initialValues={{ firstName: '', lastName: '' }}>
{() => (
<form>
<InputField name="firstName" />
<InputField name="lastName" />
<Field type="checkbox" value="1" name="send_me_spams" />
<AutoSave onSave={onSave} render={({ isSaving }) => (isSaving ? <Spinner /> : '')} />
</form>
)}
</Formik>
</div>
)
Here is what I'm doing:
import { useRef, useEffect, useState } from 'react' import { connect } from 'formik' import isEqual from 'lodash.isequal' const usePrevious = value => { const ref = useRef() useEffect(() => { ref.current = value }) return ref.current } const AutoSave = ({ formik: { values }, onSave, render }) => { const previousValues = usePrevious(values) const [saving, setSaving] = useState(false) function callback(value) { setSaving(false) return value } function save() { if (previousValues && Object.keys(previousValues).length && !isEqual(previousValues, values)) { setSaving(true) onSave(values).then(callback, callback) } } useEffect(() => { save() }, [values]) return render({ isSaving }) } export default connect(AutoSave)But I also tweak my input fields, so they only trigger "onChange" event on blur. Thus, no need for
debounce.export const InputField = (props) => <Field component={InputComponent} {...props} /> function InputComponent({ field, form: { handleChange, handleBlur }, ...props }) { const onBlur = e => { handleChange(e); handleBlur(e) } return <input {...field} onChange={null} onBlur={onBlur} {...props} /> }And finally:
import React from 'react'; import { Formik } from 'formik' import { InputField } from '...' import AutoSave from '...' import apiCall from '...' const onSaving = values => apiCall(values) /* must return a promise */ .then(...) .catch(...) const App = () => ( <div> <h1>My form</h1> <Formik initialValues={{ firstName: '', lastName: '' }}> {() => ( <form> <InputField name="firstName" /> <InputField name="lastName" /> <Field type="checkbox" value="1" name="send_me_spams" /> <AutoSave onSave={onSave} render={({ isSaving }) => (isSaving ? <Spinner /> : '')} /> </form> )} </Formik> </div> )
Hi... I am new to Formik. I liked your approach of onBlur instead of debounce. I was trying it on my end but got the error that says.. "You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly". Can you please share working example if you have any?
@oyeanuj You just need to ensure to pass a value to your input. even an empty one ('') coz null will throw the error you mentioned
For instance, you can do: function InputComponent({ field: { value='', name }, ...
Hi @gtournie Thanks for your response! 馃憤 This is Vikram and not Anuj :-) I guess the issue was null in handleChange. I fixed that by passing default handleChange from Formik props. But, then faced issues with function that called axios.put throwing "not a function" error after saving the record once. That is, autosave works first time after onblur is fired, but then re-executes again resulting in that error. I am not sure why. So I am going to try custom onBlur from comments on other issues (this and this). Hope I can get something working! Thanks!
Seeing as how in 2019 this is now how the majority of mobile forms work my opinion is that this should be a mainline option presented by the plugin itself. I know that this plugin isn't explicitly for react-native but not having this feature somewhat excludes react native from working well with this plugin. Just my two cents here. I wouldn't mind adding a PR to react-native-formik to address the lack of this feature.
Most helpful comment
The goal of my example to show the pattern you might use to handle pretty much kind of extended Formik behavior. Much like React Router 4, Formik is just a React component. This means that the problems you run into can usually be solved with just React and don鈥檛 require expanding the API. That being said, if there is a community need, I鈥檇 probably codify and bless a solution by creating formik-autosave