@lightpohl

Persist Formik State on Refresh with React Hooks

February 23rd, 2021

Hey there! If you're just looking for the full code example, jump down to Putting It All Together.

Out of all the different React form libraries I've used in different projects, Formik 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 two libraries here (react-fast-compare and use-debounce) to simplify the example code, but feel free to roll your own code or use an equivalent library as a replacement.

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.

...

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

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