Use localStorage and Formik to supercharge your form experience

Frank Meszaros
5 min readMay 21, 2021

--

Formik Logo

Context

If you’ve ever built out the User Interface of a web application, there’s a good chance you have had to build a form of some kind. Additionally, there are cases where maybe the user will navigate away from the form, or just accidentally reload the page. In both of these cases, a vanilla form’s state will be wiped away and they’ll have to re-enter their entire form. Talk about a crummy user experience!

Solution

There’s a few different ways one might be able to knock this problem out, but I’ve found that if you need to simply persist the data in a form between page reloads and other asynchronous workflows, localStorage does a fine job.

What we’ll use

Set up

  • Make sure you have create-react-app installed on your machine. Docs to get started are here
  • Inside of your newly created project. Install Formik using yarn add formik

That should be everything necessary to get your project off the ground! Let’s get building.

Build the form

We will build a simple form that has a few interactive elements to test with. First, remove the boilerplate from App.js and replace it with the following:

// App.jsimport React from 'react';import { Field, Formik, Form } from 'formik';const LOCAL_STORAGE_KEY = 'customLocalStorageKey';
const INITIAL_VALUES = { foo: '', bar: [] };
const MyForm = ({ ...props }) => {
return (
<Form>
<div style={{ display: 'flex', justifyContent: 'space-between'}}>
<div style={{ flex: 1 }}>
<h2>Form state:</h2>
<pre>{JSON.stringify(props.values, null, 2)}</pre>
</div>
<div>
<h2>LocalStorage:</h2>
<div>
<pre>{localStorage.getItem(LOCAL_STORAGE_KEY)}</pre>.
</div>
</div>
</div>
<div>
<h2>My form</h2>
<div style={{ margin: 5 }} role="group" aria-labelledby="my-radio-group">
<label>
<Field type="radio" name="foo" value="One" />
One
</label>
<label>
<Field type="radio" name="foo" value="Two" />
Two
</label>
</div>
<div style={{ margin: 5 }} role="group" aria-labelledby="my-checkbox-group">
<label>
<Field type="checkbox" name="bar" value="Red" />
Red
</label>
<label>
<Field type="checkbox" name="bar" value="Blue" />
Blue
</label>
</div>
</div>

<div style={{ marginTop: 10 }}>
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</div>
</Form>
);
};
function App() {
const handleSubmit = React.useCallback((values) => {
console.log('Submitting form!!!!');
}, []);

return (
<div className="App">
<h1 style={{ textAlign: 'center' }}>LocalStorage state</h1>
<Formik
enableReinitialize
initialValues={INITIAL_VALUES}
onSubmit={handleSubmit}
>
{(props) => <MyForm {...props}/>}
</Formik>
</div>
);
}
export default App;

Your React app should look something like this after this is added:

Don’t worry that there’s nothing in the LocalStorage section yet, that’s because we haven’t created anything there. We’ll do that next.

Build a custom hook

Next we can build a custom hook that will plug in to our form in the next step. In your src directory, create another file called hook.js

// hook.jsimport React from 'react';export const useLocalStorageState = ({ key, value }) => {
const parsedLocalStorage = JSON.parse(localStorage.getItem(key) || '{}');
const initialValue = Object.keys(parsedLocalStorage).length > 0 ? parsedLocalStorage : value;
const [localStorageState, setLocalStorageState] = React.useState(initialValue); const handleUpdateLocalStorageState = React.useCallback((x) => {
setLocalStorageState(x);
localStorage.setItem(key, JSON.stringify(x));
},
[key]
);
return [localStorageState, handleUpdateLocalStorageState];
};

Let’s take a look at this piece by piece:

  • We first create parsedLocalStorage to see if anything already exists in localStorage.
  • Next, we use the incoming value or the existing value in localStorage as our initialValue .
  • After we have our initialValue , we use it to create a state variable localStorageState to track changes to local storage over time.
  • Then, we create a callback called handleUpdateLocalStorageState to give people the ability to update localStorage as needed.
  • We return [localStorageState, handleUpdateLocalStorageState] to the client so they can access and update the value stored in localStorage.

Putting it together

First, we need to import our new hook in App.js :

// App.js// The other imports ... import { useLocalStorageState } from './hook';// Constants and Components ...

Next, we need to update App to use our fancy new hook. We will do this by declaring our hook, and then replacing the initialValues prop on our Formik component from INITIAL_VALUES to the initialValues variable provided on useLocalStorageState .

function App () {
// handleSubmit ...
const [initialValues, handleUpdateForm] = useLocalStorageState({ key: LOCAL_STORAGE_KEY, value: INITIAL_VALUES });

return (
<div className="App">
<h1 style={{ textAlign: 'center' }}>LocalStorage state</h1>
<Formik enableReinitialize initialValues={initialValues} onSubmit={handleSubmit}>

Finally, we need to connect our handleUpdateForm handler from above to our <MyForm> component. We do this by creating a new prop on <MyForm> called saveForm and passing handleUpdateForm to it.

So, we need to change <MyForm> from:

// ...
{(props) => <MyForm {...props} />}// ...

to

{(props) => <MyForm saveForm={handleUpdateForm} {...props} />}

Next, we need to reference the new prop in our <MyForm> component. Update the declaration of <MyForm> to look like this:

const MyForm = ({ saveForm, ...props }) => {
React.useEffect(() => {
saveForm(props.values);
}, [props.values, saveForm]);
const handleReset = React.useCallback(() => {
saveForm(INITIAL_VALUES);
}, [saveForm]);
return (
// Component logic here ...

Now, we have a useEffect that listens to changes on props.values and will update localStorage accordingly.

Additionally, we now have a new handleReset callback that we will pass to our button to reset both the form and localStorage’s state.

Finally, we need to connect handleReset to our <button> . So in <MyForm> , find the button of type="reset”:

<button type="reset">
Reset
</button>

And change it to:

<button onClick={handleReset} type="reset">
Reset
</button>

The final result

Now that we have our form in place, our custom hook built and passed to our form, we should be able to see the form in action!

Conclusion

The full example for this can be found here. Thanks for reading!

Did you enjoy? I’d appreciate a clap. 👏

Did something break for you? I’d really appreciate a comment. 🐶

--

--