Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

Using localStorage with React Hooks

7 min read 2050

Using localStorage React Hooks

localStorage is one of the two mechanisms of a browsers’ web storage. It allows users to save data as key/value pairs in the browser for later use.

Unlike the sessionStorage mechanism, which persists data in the browser storage as long as the current browser tab is running, localStorage does not clear data when the browser closes.

This makes it ideal for persisting data not bound to the current browser tab.

Developers often implement localStorage when adding a dark mode feature to an application, persisting a to-do item, or persisting a user’s form input values, among many other use cases.

In this guide, we cover how to use localStorage to persist a user’s form input in the browser storage using React Hooks. We’ll also cover how to create a custom React Hook to share similar logic between multiple components.

localStorage with React Hooks prerequisites

To follow this guide, ensure you have a basic understanding of React and React Hooks. Also, ensure you have Node.js installed on your computer.

Initial localStorage project setup

Working with a fresh React application, let’s head over to the computer terminal and run the following command to create a new React project:

npx create-react-app localstorage-react-hook

Once the project folder generates, open it with a code editor and start the development server by running the npm start command.

The project should launch in the browser at http://localhost:3000/.

Creating a React form component

As mentioned earlier, we will use the localStorage to persist a user’s form input in the browser storage.

We made a custom demo for .
No really. Click here to check it out.

Like every React application, our focus is on the src folder. So, let’s delete all the files inside the src and create an index.js file inside src to avoid a frontend break.

Then, add the following code to index.js:

import React from "react";
import ReactDOM from "react-dom";

import App from "./components/App";
// styles
import "./app.css";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

Notice that we imported a CSS file to add styling to the app. So, let’s create an app.css folder in the src folder.

Copy the styles from the localstorage-react-hook-project and add them to the app.css file.

Next, create a components folder in the src folder to hold the component files. Then, add an App.js file and a Form1.js file. The App.js file is the root and parent component while Form1.js will hold the form inputs.

Add the following code in the components/App.js file:

import Form1 from "./Form1";

const App = () => {
  return (
    <div className="container">
      <h1>localStorage with React hooks</h1>
      <Form1 />
    </div>
  );
};
export default App;

And finally, add this code to the components/Form1.js file:

import { useState } from "react";

const Form1 = () => {
  const [name, setName] = useState("");

  return (
    <form>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Full name"
        aria-label="fullname"
      />
      <input type="submit" value="Submit"></input>
    </form>
  );
};

export default Form1;

After saving the files, test the project and you should see this rendering:

UI Shows Form and Button With Code Below Showing Component Rendering

The code above is the simplest implementation of the form inputs in React. By using the useState React Hook to control the component, we keep the input state up-to-date on every keystroke, as seen above.

But, once we trigger a page refresh, the input data clears, which is expected.

To persist the input data so it’s available on a page reload or on subsequent revisits, we must save the data in the localStorage.

Saving the form input data in localStorage

localStorage gives us access to a browser’s Storage object. The Storage object has methods available to save, read, and remove data, among many other actions.

To see a list of the Storage methods, open the browser console and type localStorage. After pressing enter, the methods become available on the Storage object’s prototype.

Using the setItem() method

To store the form input data in the browser storage, we must invoke the setItem() storage method by using the following syntax:

localStorage.setItem("key", "value")

The browser storage only accepts data-type strings. So, for values of different data types like the object or array, we must convert it to a JSON string using JSON.stringify().

Using the useEffect Hook to perform side effects

We can also use the useEffect React Hook to perform side effects, such as storing data in the browser storage. This makes this Hook a perfect place to call the setItem method.

Open the components/Form1.js file and add the following code above the return statement:

useEffect(() => {
  // storing input name
  localStorage.setItem("name", JSON.stringify(name));
}, [name]);

Ensure to import the useEffect from React like so:

import { useState, useEffect } from "react";

Here, we’ve assigned a key, "name", and a dynamic value from the state variable, which is name.

The initial value of the name state variable defaults to an empty string:

const [name, setName] = useState("");

Using JSON.stringify in the setItem is optional when saving string data to the storage:

localStorage.setItem("name", JSON.stringify(name));

However, JSON.stringify is required if the value is a different data type, like an object or array.

Now, save the file and test the project; we should see the following render:

useEffect Rendered In Browser With Code Below

On every keystroke, the input value saves in the local storage because the useEffect Hook holding the setItem storage method runs on the first component render and after every state change.

However, on a page reload, the value in the storage returns to an empty string. This is happening because we’ve assigned a default empty string to the state variable, name. Hence, React uses the empty value on the initial render.

Now, instead of assigning an empty string, we must get the updated state value at every point from the storage and assign it as the default state value.

Reading data from the localStorage

On an initial page load, instead of assigning an empty string to the name state variable, we must assign a function that accesses the local storage, retrieve the saved value, and use that value as the default.

Using the getItem() method

Update the useState Hook in the components/Form1.js file:

const [name, setName] = useState(() => {
  // getting stored value
  const saved = localStorage.getItem("name");
  const initialValue = JSON.parse(saved);
  return initialValue || "";
});

Here, we use the getItem() storage method to retrieve data from the local storage. The JSON.parse() used in the code deserializes the returned JSON string from the storage.

Both the JSON.Stringify and the JSON.parse are optional when working with string values (as seen in our case). However, other data types, like objects and arrays, require them.

Save the file and test the project. The input data should be available in the form field on a page reload or a later page visit.

Input Remains On Page Refresh

Creating a custom React Hook to persist form inputs

Sometimes we might want to render and persist more form inputs, such as a text input and a checkbox input, in a different component.

While we could easily copy the logic from the one we’ve already created and use it in the new component, it’s not always practicable, especially if we decide to create more of these inputs.

Instead, React allows us to extract and share similar logic between components using custom Hooks.

In this section, we will learn how to create a custom Hook to persist form inputs in multiple components.

Let’s start by creating another form. In the src/components folder, create a new file called Form2.js, and add the following code:

import { useState } from "react";

const Form2 = () => {
  const [name, setName] = useState("");
  const [checked, setChecked] = useState(false);

  return (
    <form>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Full name"
        aria-label="fullname"
      />
      <label>
        <input
          type="checkbox"
          checked={checked}
          onChange={(e) => setChecked(e.target.checked)}
        />{" "}
        Not a robot?
      </label>
      <input type="submit" value="Submit"></input>
    </form>
  );
};

export default Form2;

Then, import and use the component in the components/App.js file:

// ...
import Form2 from "./Form2";

const App = () => {
  return (
    <div className="container">
      {/* ... */}
      <Form2 />
    </div>
  );
};
export default App;

Save the files and view the form in the frontend.

Input Field With Checkbox In Form

Interacting with this form does not persist the state value in localStorage since we don’t have the logic yet.

So, let’s define a single logic to manage all our form inputs.

Extracting the localStorage logic

To begin extracting the localStorage logic, create a file called useLocalStorage.js in the src folder and add the following code:

import { useState, useEffect } from "react";

function getStorageValue(key, defaultValue) {
  // getting stored value
  const saved = localStorage.getItem(key);
  const initial = JSON.parse(saved);
  return initial || defaultValue;
}

export const useLocalStorage = (key, defaultValue) => {
  const [value, setValue] = useState(() => {
    return getStorageValue(key, defaultValue);
  });

  useEffect(() => {
    // storing input name
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
};

Taking a closer look at the code above, we’ve only extracted the storage logic from the components/Form1.js file. We’ve done nothing special.

By creating a custom Hook called useLocalStorage, we maintain all of the storage logic we have in the Form1 component.

The useLocalStorage Hook expects two arguments: the key and the defaultValue. This means we expect to pass these values when calling the Hook in our different components.

Note that you can name your custom Hook anything, but ensure you start with use.

Using the useLocalStorage custom Hook

In the components/Form1.js file, replace the logic above the return statement with the custom Hook so you have the following:

import { useLocalStorage } from "../useLocalStorage";

const Form1 = () => {
  const [name, setName] = useLocalStorage("name", "");

  return (
    <form>
      {/* ... */}
    </form>
  );
};

export default Form1;

After importing the custom Hook, we can use it and pass along the unique key and the default value, which, in this case, is an empty string.

If we do the same for the Form2 component in the components/Form2js file, we should have the following:

import { useLocalStorage } from "../useLocalStorage";

const Form2 = () => {
  const [name, setName] = useLocalStorage("name2", "");
  const [checked, setChecked] = useLocalStorage("checked", false);

  return (
    <form>
      {/* ... */}
    </form>
  );
};

export default Form2;

Save all the files and test the project. We should be able to persist all the form inputs in localStorage.

Side-By-Side Render and Code Showing Persisting Form Inputs With localStorage

Good job!

Problems accessing localStorage for a server-side rendered application

When working with a framework like Next.js that executes code on the server-side, using localStorage gets an error stating, “window is not defined.”

The localStorage as used in our code is a built-in property of the window object, window.localStorage.

In our code, we ignored the window while accessing the localStorage because it’s a global object; we can choose to include the window object because it’s optional.

Now, this window object is not available on the server side but rather the client side/browser, which prompts the error.

To fix the error on the server side, check whether the window object is defined or not. This way, our code only runs on the environment where the window is available.

Open the src/useLocalStorage.js file and update the getStorageValue() function so you have the following:

function getStorageValue(key, defaultValue) {
  // getting stored value
  if (typeof window !== "undefined") {
    const saved = localStorage.getItem(key);
    const initial = saved !== null ? JSON.parse(saved) : defaultValue;
    return initial;
  }
}

Don’t forget that we’ve also used the localStorage inside the useEffect Hook in the useLocalStorage.js file.

But in this case, the localStorage is safe because the useEffect Hook only runs on the client-side where we have access to the window object.

Test the project to ensure everything still works as expected.

Conclusion

We’ve covered how to use the localStorage to persist data in a browser using the React Hooks. We’ve also learned how to create a custom Hook to extract component logic into reusable functions.

If you enjoyed this guide, share it around the web. And, if you have any questions or contributions, please share them via the comment section.

Find the entire source code for the project here.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

Leave a Reply