Persist Formik State on Refresh with React Hooks
Tags: react, formik
Published: 2021-02-23 23:33:08

> Hey there! If you're just looking for the full code example, jump down to [Putting It All Together](#putting-it-all-together).

Out of all the different React form libraries I've used in different projects, [Formik](https://formik.org/) is currently my favorite. It's intuitive and provides lower-level API access to workaround situations a high-level only API might not anticipate.

If you're building a large form, or one that will be filled out slowly over some time, persisting its state across sessions is really important. It's especially important if you anticipate much usage on mobile where apps are often removed from memory unexpectedly when users are multitasking.

Fortunately, Formik makes adding that behavior simple, and that behavior can also be consolidated into one reusable component that you can drop into any Formik form provider.

## Saving to Local Storage

Let's start by saving the form state into local storage when the form is updated. We're going to use a two libraries here ([`react-fast-compare`](https://www.npmjs.com/package/react-fast-compare) and [`use-debounce`](https://www.npmjs.com/package/use-debounce)) to simplify the example code, but feel free to roll your own code or use an equivalent library as a replacement.

```jsx
import {useEffect, useRef} from 'react';
import {useFormikContext} from 'formik';
import isEqual from 'react-fast-compare';
import {useDebouncedCallback} from 'use-debounce';

const FormikPersist = ({name}) => {
  const {values} = useFormikContext();
  const prefValuesRef = useRef();

  const onSave = (values) => {
    window.localStorage.setItem(name, JSON.stringify(values));
  };

  const debouncedOnSave = useDebouncedCallback(onSave, 300);

  useEffect(() => {
    if (!isEqual(prefValuesRef.current, values)) {
      debouncedOnSave.callback(values);
    }
  });

  useEffect(() => {
    prefValuesRef.current = values;
  });

  return null;
};

export default FormikPersist;
```

Our `FormikPersist` component takes one prop: a `name` to be the unique identifier we will use to store the form in `window.localStorage`. Since our component will live underneath a Formik provider, we can use `useFormikContext` to access the current values of the form. On each render of our component, we compare the previous list of values with the current values and save the latest values to local storage if they differ. We use a debounced version of our `onSave` function to write to local storage so as to not write too many times in quick succession.

The hardest part is over—the rest is even easier.

## Rehydrating on Refresh

Now we just need to check our local storage on component mount and initialize the form values if we find them.

```jsx
...

  // We're now grabbing 'setValues' as well
  const {values, setValues} = useFormikContext();

  ...

  useEffect(() => {
    const savedForm = window.localStorage.getItem(name);

    if (savedForm) {
      const parsedForm = JSON.parse(savedForm);

	  prefValuesRef.current = parsedForm;
	  setValues(parsedForm);
    }
  }, [name, setValues]);

...
```

> Tip: one alteration you might want to make to this component is to only rehydrate form values that were saved within a certain time span (e.g. the last day or the last week). To do that we would save a timestamp with the form values and compare that timestamp with the current time in this `useEffect`.

## Putting It All Together

We now have our complete `FormikPersist` component that we can place under a Formik provider to handle persisting our form state on refresh.

```jsx
import {useEffect, useRef} from 'react';
import {useFormikContext} from 'formik';
import isEqual from 'react-fast-compare';
import {useDebouncedCallback} from 'use-debounce';

const FormikPersist = ({name}) => {
  const {values, setValues} = useFormikContext();
  const prefValuesRef = useRef();

  const onSave = (values) => {
    window.localStorage.setItem(name, JSON.stringify(values));
  };

  const debouncedOnSave = useDebouncedCallback(onSave, 300);

  useEffect(() => {
    const savedForm = window.localStorage.getItem(name);

    if (savedForm) {
      const parsedForm = JSON.parse(savedForm);

      prefValuesRef.current = parsedForm;
      setValues(parsedForm);
    }
  }, [name, setValues]);

  useEffect(() => {
    if (!isEqual(prefValuesRef.current, values)) {
      debouncedOnSave.callback(values);
    }
  });

  useEffect(() => {
    prefValuesRef.current = values;
  });

  return null;
};

export default FormikPersist;
```

```jsx
// inside some render function
<Formik {...props}>
  <Form {...props}>
    <FormikPersist name="our-form" />
  </Form>
</Formik>
```

And that's it! Add this to your form and you'll make mobile visitors of your website much happier if their browser tab is thrown to the wind. I'm looking at you, [iOS 13](https://www.theverge.com/2019/10/31/20942043/apple-ios-13-iphone-11-pro-ram-memory-management-app-background-refresh)