Use localStorage and Formik to supercharge your form experience
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
- React — Our frontend library of choice
- Formik — form library for React
- localStorage — “A
Storage
object which can be used to access the current origin's local storage space” (developer.mozilla).
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 variablelocalStorageState
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. 🐶