Skip to content

Instantly share code, notes, and snippets.

@jaredpalmer
Last active December 29, 2022 01:22
Show Gist options
  • Save jaredpalmer/56e10cabe839747b84b81410839829be to your computer and use it in GitHub Desktop.
Save jaredpalmer/56e10cabe839747b84b81410839829be to your computer and use it in GitHub Desktop.
Formik-Autosave
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>
@miczed
Copy link

miczed commented Oct 11, 2018

I am trying to get the form to autosave only if it passes the validation first. Any advice on how to do this? A check on nextProps.formik.isValid does not work since the validation hasn't happened yet.

@Mr-Chilly
Copy link

Mr-Chilly commented Nov 16, 2018

Just incase anyone is interested - I've npm'd this for easy use:

https://github.com/Mr-Chilly/withFormikAutoSave

@dmitru
Copy link

dmitru commented Dec 25, 2018

I am trying to get the form to autosave only if it passes the validation first. Any advice on how to do this? A check on nextProps.formik.isValid does not work since the validation hasn't happened yet.

Here's what works for me (using Formik's connect() instead of static contextTypes):

import React from 'react'
import * as _ from 'lodash'
import { connect } from 'formik'


export default connect(
  class FormikAutoSave extends React.Component {
    state = {
      isSaving: false,
    }

    componentDidUpdate(prevProps) {
      if (!_.isEqual(prevProps.values, this.props.values)) {
        this.save()
      }
    }

    save = _.debounce(() => {
      if (!this.props.formik.isValid) {
        return
      }

      this.setState({ isSaving: true, saveError: undefined }, () => {
        Promise.resolve(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.props.render(this.state) : null
    }
  },
)

@hboylan
Copy link

hboylan commented Apr 11, 2020

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])

@swashata
Copy link

swashata commented Jun 16, 2020

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>
	);
}

@mini1612
Copy link

mini1612 commented Jul 8, 2020

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?

@mini1612
Copy link

mini1612 commented Jul 8, 2020

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

@asifsaho
Copy link

asifsaho commented Jun 25, 2021

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} />

@wookiehunter
Copy link

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?

@SPodjasek
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment