-
-
Save jaredpalmer/56e10cabe839747b84b81410839829be to your computer and use it in GitHub Desktop.
import React from 'react'; | |
import PropTypes from 'prop-types' | |
import debounce from 'lodash.debounce' // or whatevs | |
import isEqual from 'lodash.isEqual' | |
class AutoSave extends React.Component { | |
static contextTypes = { | |
formik: PropTypes.object | |
} | |
state = { | |
isSaving: false, | |
} | |
componentWillReceiveProps(nextProps, nextContext) { | |
if (!isEqual(nextProps.values, this.props.values)) { | |
this.save() | |
} | |
} | |
save = debounce(() => { | |
this.setState({ isSaving: true, saveError: undefined }) | |
this.props.onSave(this.props.values) | |
.then( | |
() => this.setState({ isSaving: false, lastSaved: new Date() }), | |
() => this.setState({ isSaving: false, saveError }) | |
) | |
}), 300) | |
} | |
render() { | |
return this.props.render(this.state) | |
} | |
} | |
} | |
// Usage | |
import React from 'react'; | |
import { Formik, Field, Form } from 'formik' | |
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now' | |
const App = () => | |
<div> | |
<h1>Signup form</h1> | |
<Formik | |
initialValues={{ firstName: '', lastName: ''} | |
onSubmit={values => { | |
setTimeout(() => { | |
alert(JSON.stringify(values, null,2)) | |
}, 500) | |
} | |
render={() => | |
<Form> | |
<Field name="firstName" /> | |
<Field name="lastName" /> | |
<button type="submit">Submit</button> | |
<AutoSave | |
onSave={values => CallMyApi(values) /* must return a promise 😎 */}\ | |
debounce={1000} | |
render={({isSaving, lastSaved, saveError }) => | |
<div> | |
{!!isSaving | |
? <Spinner/> | |
: !!saveError | |
? `Error: ${saveError}` | |
: lastSaved | |
? `Autosaved ${distanceInWordsToNow(lastSaved)} ago` | |
: 'Changes not saved'} | |
</div> | |
} | |
/> | |
</Form> | |
} | |
/> | |
</div> |
I'm using the useFormik
hook so I used the following to debounce the form submit.
useEffect(() => {
if (!form.dirty) return
const handler = setTimeout(form.submitForm, 1000)
return () => clearTimeout(handler)
}, [form.dirty, form.submitForm])
Here's what works for me
import { useEffect } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { useFormikContext } from 'formik';
export function AutoSave() {
const formik = useFormikContext();
const [debouncedSubmitCaller] = useDebouncedCallback((ctx: typeof formik) => {
if (ctx.isValid) {
console.log('autosave');
ctx.submitForm();
}
}, 500);
useEffect(() => {
if (formik.isValid && formik.dirty && !formik.isSubmitting) {
debouncedSubmitCaller(formik);
}
}, [debouncedSubmitCaller, formik]);
return null;
}
USAGE
import React from 'react';
import { Formik } from 'formik';
import { Form } from 'formik-antd';
import { AutoSave } from './AutoSave';
export default function SomeForm(props: any) {
const { initialValues, onSubmit } = props;
return (
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
>
<Form>
{ /** Other Stuff */ }
<AutoSave />
</Form>
</Formik>
);
}
Here's what works for me
import { useEffect } from 'react'; import { useDebouncedCallback } from 'use-debounce'; import { useFormikContext } from 'formik'; export function AutoSave() { const formik = useFormikContext(); const [debouncedSubmitCaller] = useDebouncedCallback((ctx: typeof formik) => { if (ctx.isValid) { console.log('autosave'); ctx.submitForm(); } }, 500); useEffect(() => { if (formik.isValid && formik.dirty && !formik.isSubmitting) { debouncedSubmitCaller(formik); } }, [debouncedSubmitCaller, formik]); return null; }USAGE
import React from 'react'; import { Formik } from 'formik'; import { Form } from 'formik-antd'; import { AutoSave } from './AutoSave'; export default function SomeForm(props: any) { const { initialValues, onSubmit } = props; return ( <Formik initialValues={initialValues} onSubmit={onSubmit} > <Form> { /** Other Stuff */ } <AutoSave /> </Form> </Formik> ); }
submitForm is getting invoked indefinite times, after first time form is submitted. however when I comment out submit handler line then debouncedSubmitCaller is getting called only when there is change in the form field. This is happening because there is dependency of formik on useEffect some property of formik is getting changes which is turn again invoking debouncedcallback handler. How can I fix this?
Oh I am able to get rid of the issue. Just passed formik.values as dependencies instead of formik. It works. Thanks for sharing the information
I am using this to autosave, showing two different messages for success and error. I hope it would be helpful for you.
import { AkCheck, AkError } from '@akelius-con/react-ui-kit-icons';
import { Box, createStyles, makeStyles, Typography } from '@material-ui/core';
import cn from 'classnames';
import { FormikProps } from 'formik';
import { debounce, isEqual } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
formik: FormikProps<any>;
debounceMs?: number;
error: null | boolean;
}
const FormikAutoSave = ({ formik, debounceMs = 1500, error }: Props) => {
const classes = useStyles();
const { t } = useTranslation();
const debouncedSubmit = useCallback(
debounce(() => {
if (!formik.isValid || !formik.dirty) return false;
return formik.submitForm();
}, debounceMs),
[formik.submitForm, formik.isValid, formik.initialValues, formik.values, debounceMs],
);
useEffect(() => {
debouncedSubmit();
return debouncedSubmit.cancel;
}, [debouncedSubmit, formik.values]);
return (
<div className="spinner">
{formik.isSubmitting && (
<div>saving in progress</div>
)}
{!formik.isSubmitting && (
<div>
{error === false && (
<>
Success
</>
)}
{error === true && (
<>
Error
</>
)}
</div>
)}
</div>
);
};
export default FormikAutoSave;
// usages: <FormikAutoSave error={autoSaveError} formik={formik} />
Hi, I am working on some code that uses this for saving filter values. When I select a record in a filtered list and then return to the list, using browser back button, only 2 of the 4 filter values are applied. Has anyone else come across this and resolved?
There's one tiny problem with the solutions above - when you rely solely on formik.dirty
, you will miss any changes that result in a field value equal to initialValues
, as per specs:
dirty: boolean
Returns true if values are not deeply equal from initial values, false otherwise. dirty is a readonly computed property and should not be mutated directly.
So when your Formik doesn't have enableReinitialize
prop enabled (by default, it doesn't) and you change some field, autosave submits your form, and then you change that field once more to value equal with initial value - it won't detect that as a change.
For most implementations, that may not be the case, or maybe you can use enableReinitialize
- but I didn't want to, as users can lose the focused field when reinitialization happens. So I came up with another implementation, which holds the last submitted values in its state, and uses isEqual
from react-fast-compare
just as Formik internally does.
You can find this implementation here: https://gist.github.com/SPodjasek/c5354da2e897daa14654674ab21c9b72
Here's what works for me (using Formik's
connect()
instead ofstatic contextTypes
):