tutorial // Sep 03, 2021
How to Use Local Storage to Persist Form Data in JavaScript
How to utilize local storage to improve user experience by backing up and restoring form data for users.
Getting started
For this tutorial, we're going to use the CheatCode Next.js Boilerplate as a starting point for our work. To get started, let's a clone a copy:
Terminal
git clone https://github.com/cheatcode/nextjs-boilerplate.git
Next, cd
into the project and install the dependencies:
Terminal
cd nextjs-boilerplate && npm install
Finally, start up the dev server:
Terminal
npm run dev
With that, we're ready to get started.
Building a form
Before we start persisting form data, we need a form that we can pull data from and load it back into. To start, we're going to add a new page component via React to house our form:
/pages/index.js
import React from "react";
import StyledIndex from "./index.css";
class Index extends React.Component {
state = {};
render() {
return (
<StyledIndex>
<form>
// We'll render our form fields here...
</form>
</StyledIndex>
);
}
}
export default Index;
In a Next.js app, all files and folders under the /pages
folder double as routes or URLs in the browser. Here, by creating our page at /pages/index.js
, in the browser, we can expect to access our page at http://localhost:5000/
(the index or root of our application).
For our component, we're using the class-based approach in React as opposed to the function-based approach (we'll benefit from this later when loading data into our form from local storage). Here, inside the render()
method, we're rendering a styled component <StyledIndex />
which we'll use to apply some basic styling to our <form></form>
. Let's take a look at that file now:
/pages/index.css.js
import styled from "styled-components";
export default styled.div`
form {
max-width: 50%;
}
`;
styled-components
is a library that helps to easily add CSS to our React components. It works by generating React components automatically containing some HTML element and then attaching the styles we provide (here, what's between the backticks) to that element. Above, we import styled
from the styled-components
package (automatically installed in the boilerplate we cloned earlier) and then create a new styled component containing an HTML <div></div>
element.
Though it may not look like it, here, styled.div
is technically a function styled.div()
. The syntax here is a convenience feature in JavaScript that allows us to call a function only expecting a single argument in the type of a string by dropping the parentheses and using backticks around the string being passed. That string here contains our CSS which limits the width of our form to only be 50% of the page.
/pages/index.js
import React from "react";
import StyledIndex from "./index.css";
class Index extends React.Component {
state = {};
render() {
return (
<StyledIndex>
<form>
// We'll render our form fields here...
</form>
</StyledIndex>
);
}
}
export default Index;
Back in our component, we import and render our styled component, in this case wrapping it around an HTML <form></form>
tag where we'll render our form fields.
/pages/index.js
import React from "react";
import StyledIndex from "./index.css";
class Index extends React.Component {
state = {};
render() {
return (
<StyledIndex>
<form>
<div className="row">
<div className="col-sm-6">
<div className="mb-3">
<label className="form-label">First Name</label>
<input
type="text"
name="firstName"
className="form-control"
/>
</div>
</div>
<div className="col-sm-6">
<div className="mb-3">
<label className="form-label">Last Name</label>
<input
type="text"
name="lastName"
className="form-control"
/>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<div className="mb-3">
<label className="form-label">Favorite Ice Cream Flavor</label>
<select
className="form-select"
>
<option value="chocolate">Chocolate</option>
<option value="vanilla">Vanilla</option>
<option value="strawberry">Strawberry</option>
<option value="neopolitan">Neopolitan</option>
</select>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<div className="mb-5">
<label className="form-label">Toppings</label>
<div class="form-check">
<input
className="form-check-input"
type="checkbox"
value="sprinkles"
/>
<label className="form-check-label">Sprinkles</label>
</div>
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
value="cherry"
/>
<label className="form-check-label">Cherry</label>
</div>
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
value="hotFudge"
/>
<label className="form-check-label">Hot Fudge</label>
</div>
</div>
</div>
</div>
<button className="btn btn-primary" style={{ marginRight: "10px" }}>
Submit
</button>
<button
className="btn btn-light"
type="button"
>
Reset Form
</button>
</form>
</StyledIndex>
);
}
}
export default Index;
Filling out the body of our form, here, we've added a mix of HTML inputs to demonstrate retrieving data from a form and then setting it back after a page refresh from local storage. We have six fields:
- A first name text input
- A last name text input
- A select input for selecting your favorite ice cream flavor
- A series of checkboxes for checking off ice cream toppings
While our form will render on-screen and be fillable, if we refresh the page, any data we input into the form will be lost. Next, in order to avoid this, we're going to learn how to store our data on our React component's state first and then back that up to local storage.
Setting data on state and local storage
Above, we set up a page component that renders our form fields. Now, we want to capture the value from the inputs in that form and set them on our component's state as well as local storage. To do it, we're going to add a function that we can call from all of our inputs that will centralize the setting of input values on state and local storage.
Terminal
import React from "react";
import StyledIndex from "./index.css";
class Index extends React.Component {
state = {};
handleUpdateState = (field = "", value = "") => {
this.setState({ [field]: value }, () => {
if (localStorage) {
localStorage.setItem("formData", JSON.stringify(this.state));
}
});
};
render() {
const { firstName, lastName, iceCreamFlavor, sprinkles, cherry, hotFudge } =
this.state;
return (
<StyledIndex>
<form>
<div className="row">
<div className="col-sm-6">
<div className="mb-3">
<label className="form-label">First Name</label>
<input
type="text"
name="firstName"
value={firstName}
onChange={(event) =>
this.handleUpdateState("firstName", event.target.value)
}
className="form-control"
/>
</div>
</div>
<div className="col-sm-6">
<div className="mb-3">
<label className="form-label">Last Name</label>
<input
type="text"
name="lastName"
value={lastName}
onChange={(event) =>
this.handleUpdateState("lastName", event.target.value)
}
className="form-control"
/>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<div className="mb-3">
<label className="form-label">Favorite Ice Cream Flavor</label>
<select
className="form-select"
value={iceCreamFlavor}
onChange={(event) =>
this.handleUpdateState("iceCreamFlavor", event.target.value)
}
>
<option value="chocolate">Chocolate</option>
<option value="vanilla">Vanilla</option>
<option value="strawberry">Strawberry</option>
<option value="neopolitan">Neopolitan</option>
</select>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-12">
<div className="mb-5">
<label className="form-label">Toppings</label>
<div class="form-check">
<input
className="form-check-input"
type="checkbox"
value="sprinkles"
checked={sprinkles}
onChange={(event) =>
this.handleUpdateState("sprinkles", event.target.checked)
}
/>
<label className="form-check-label">Sprinkles</label>
</div>
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
value="cherry"
checked={cherry}
onChange={(event) =>
this.handleUpdateState("cherry", event.target.checked)
}
/>
<label className="form-check-label">Cherry</label>
</div>
<div className="form-check">
<input
className="form-check-input"
type="checkbox"
value="hotFudge"
checked={hotFudge}
onChange={(event) =>
this.handleUpdateState("hotFudge", event.target.checked)
}
/>
<label className="form-check-label">Hot Fudge</label>
</div>
</div>
</div>
</div>
<button className="btn btn-primary" style={{ marginRight: "10px" }}>
Submit
</button>
<button
className="btn btn-light"
type="button"
>
Reset Form
</button>
</form>
</StyledIndex>
);
}
}
export default Index;
Here, we've added a function to our class handleUpdateState
which accepts two arguments: field
and value
. The first argument field
is the name of the field we want to set on state and value
is the value we want to assign to that field.
Inside of that function, we call to this.setState()
to update our component's state value, using a special bracket notation syntax to help us dynamically set the property we want to update on state (when setting values on state, we pass one or more key/value pairs on an object). Here, [field]
will be replaced by whatever field
string we pass in as the first argument, for example { firstName: value }
or { iceCreamFlavor: value }
.
As we'll see in a bit, this allows us to call handleUpdateState
from any form field while ensuring our behavior is consistent. Also inside this function, we pass a callback function to this.setState()
to tell React "do this after you've successfully committed our field's value to the component's state." In that function, we introduce our usage of local storage.
First, we do an if (localStorage)
to make sure that local storage is available. This is necessary because some browsers may not support local storage. This includes modern browsers running in private mode.
If localStorage
exists, we call to its .setItem
method, first passing the name of the value we want to store as the first argument and then passing the value we want to store as the second. Here, because localStorage
only supports string storage, we use JSON.stringify
to stringify the entirety of our this.state
value. We do this because we want localStorage
to be the most up-to-date representation of a user's input.
Down in our render()
method, we've added two things:
- We've used JavaScript destructuring to "pluck off" our input values from
this.state
and have assigned each value to thevalue
attribute on each of our inputs (this creates what's known as a controlled component in React). - For each input, we've added an
onChange
function which takes in a DOMevent
and calls tothis.handleUpdateState()
, passing the name of the field and its value. For the<input type="checkbox" />
elements, instead of passingevent.target.value
we passevent.target.checked
.
Now, if we start typing into our form, we should see our formData
value updating in the browser's local storage:
We're almost done. To wrap up and make this useful, next, we're going to learn how to load the data we put into local storage back into our form after a page refresh.
Restoring a form from local storage
This is where our usage of the class-based React component approach pays off. In order to load data back into our form we need to know that the form exists in the DOM. To do that, we can use the componentDidMount()
lifecycle function in React to let us know our form is on screen and ready for our data.
/pages/index.js
import React from "react";
import StyledIndex from "./index.css";
class Index extends React.Component {
state = {};
componentDidMount() {
if (localStorage) {
const formDataFromLocalStorage = localStorage.getItem("formData");
if (formDataFromLocalStorage) {
const formData = JSON.parse(formDataFromLocalStorage);
this.setState({ ...formData });
}
}
}
handleUpdateState = (field = "", value = "") => { ... };
render() {
const { firstName, lastName, iceCreamFlavor, sprinkles, cherry, hotFudge } =
this.state;
return (
<StyledIndex>
<form>
...
</form>
</StyledIndex>
);
}
}
export default Index;
Inside of componentDidMount()
, we first check to see if localStorage
is defined and if it is, attempt to retrieve our formData
value from localStorage
with the .getItem()
method, passing in the name of our value formData
as a string.
Next, if we get a value, we need to convert the string we stored back into a JavaScript object. To do it, we pass formDataFromLocalStorage
to JSON.parse()
. Next, with our formData
as an object, we call to this.setState()
, passing an object who's properties are set by using the JavaScript ...
spread operator to "unpack" all of the properties on formData
onto the object we pass to .setState()
(this makes it so that each individual property is set back onto state).
With that, we can fill out our form, refresh the page, and see that our values are persisted!
Wrapping up
In this tutorial, we learned how to build a form in React that stores its contents on a component's this.state
value and backs that value up to localStorage
. We learned how to conditionally access localStorage
to avoid issues with non-supporting browsers as well as how to retrieve an existing value from localStorage
and apply it back to this.state
when the component mounts.