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.

How to Use Local Storage to Persist Form Data in JavaScript

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:

  1. A first name text input
  2. A last name text input
  3. A select input for selecting your favorite ice cream flavor
  4. 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:

  1. We've used JavaScript destructuring to "pluck off" our input values from this.state and have assigned each value to the value attribute on each of our inputs (this creates what's known as a controlled component in React).
  2. For each input, we've added an onChange function which takes in a DOM event and calls to this.handleUpdateState(), passing the name of the field and its value. For the <input type="checkbox" /> elements, instead of passing event.target.value we pass event.target.checked.

Now, if we start typing into our form, we should see our formData value updating in the browser's local storage:

OYqJB0mC1EwUH6lH/lQpQOQIcORyGj8Fk.0
Local storage being updated on form changes.

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.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode