DEV Community

Cover image for Managing Local Storage in React with useLocalStorage Hook
Saiful Islam
Saiful Islam

Posted on

Managing Local Storage in React with useLocalStorage Hook

Managing persistent data in your React application is a common requirement, and browser localStorage can help with this. In this article, we’ll break down how to create a custom React hook, useLocalStorage, for seamless local storage integration. This hook not only allows for saving, retrieving, and deleting data from localStorage, but it also provides an intuitive interface for state management.

1. Utilities for Local Storage

Before we dive into the hook, let’s create a set of utility functions to interact with localStorage. These utilities will handle setting, retrieving, and removing items while managing potential errors.

setItem: Safely Save Data to Local Storage

The setItem function takes a key and a value and stores the serialized value in localStorage.

export function setItem(key: string, value: unknown) {
  try {
    window.localStorage.setItem(key, JSON.stringify(value));
  } catch (err) {
    console.error(err);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • What it does:
    • Serializes the value using JSON.stringify.
    • Saves the data to localStorage under the provided key.
  • Error Handling: If there’s an issue (e.g., storage quota exceeded), it logs the error.

getItem: Retrieve and Parse Data

The getItem function retrieves data by a key and parses it back to its original format.

export function getItem<T>(key: string): T | undefined {
  try {
    const data = window.localStorage.getItem(key);
    return data ? (JSON.parse(data) as T) : undefined;
  } catch (err) {
    console.error(err);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • What it does:
    • Fetches data using the provided key.
    • Parses the serialized value back to its original type.
    • Returns undefined if the key doesn’t exist.
  • Type-Safety: Uses TypeScript’s generic T to ensure type consistency.

removeItem: Remove Data by Key

The removeItem function deletes the stored value associated with a key.

export function removeItem(key: string) {
  try {
    window.localStorage.removeItem(key);
  } catch (err) {
    console.error(err);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • What is does:
    • Calls localStorage.removeItem to delete the key-value pair.
    • Catches and logs any errors that occur. With these utility functions in place, we now have robust tools to interact with localStorage. Next, let’s integrate them into our custom hook.

The useLocalStorage Hook

React hooks provide a clean way to manage state and side effects. Let’s use them to create a useLocalStorage hook that combines stateful logic with our localStorage utilities.

Hook Initialization

Here’s the basic structure of the hook:

import { useState } from "react";
import { getItem, setItem, removeItem } from "@/utils/localStorage";

export default function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState(() => {
    const data = getItem(key);
    return (data || initialValue) as T;
  });

  // ...additional logic
}
Enter fullscreen mode Exit fullscreen mode
  • Parameters:
    • key: The localStorage key to interact with.
    • initialValue: The default value to use if no value exists in localStorage.
  • State Initialization:
    • The hook initializes its state (value) by calling getItem to check for existing data in localStorage.
    • If no data exists, it uses the provided initialValue.

Dispatching State Changes

The handleDispatch function manages updates to both the local state and localStorage.

type DispatchAction<T> = T | ((prevState: T) => T);

function handleDispatch(action: DispatchAction<T>) {
  if (typeof action === "function") {
    setValue((prevState) => {
      const newValue = (action as (prevState: T) => T)(prevState);
      setItem(key, newValue);
      return newValue;
    });
  } else {
    setValue(action);
    setItem(key, action);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • How It Works:
    • If action is a function, it treats it as a reducer-like updater, applying it to the current state (prevState).
    • Otherwise, it assumes action is the new value.
  • Local Storage Sync:
    • After updating the state, it stores the new value in localStorage.

Clearing the State

Sometimes, you may want to reset the state and remove the corresponding localStorage data. This is handled by the clearState function.

function clearState() {
  setValue(undefined as T);
  removeItem(key);
}
Enter fullscreen mode Exit fullscreen mode
  • What it does:
    • Resets the state to undefined.
    • Removes the associated localStorage key-value pair.

Returning the Hook’s API

Finally, the hook returns an array of three elements:

return [value, handleDispatch, clearState] as const;
Enter fullscreen mode Exit fullscreen mode
  • API:
    • 1. value: The current state.
    • 2. handleDispatch: Function to update the state.
    • 3. clearState: Function to reset the state and localStorage.

3. Using the useLocalStorage Hook

Here’s an example of how you can use this hook in a React component:

import useLocalStorage from "@/hooks/useLocalStorage";

function Counter() {
  const [count, setCount, clearCount] = useLocalStorage<number>("counter", 0);

  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
      <button onClick={() => setCount((prev) => prev - 1)}>Decrement</button>
      <button onClick={clearCount}>Reset</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • Key Features:
    • The count value is persisted across page reloads using localStorage.
    • Updates to the state (setCount) automatically sync with localStorage.
    • The clearCount function resets the counter and removes it from localStorage.

Full code

localStorage.ts code:

export function setItem(key: string, value: unknown) {
  try {
    window.localStorage.setItem(key, JSON.stringify(value));
  } catch (err) {
    console.error(err);
  }
}

export function getItem<T>(key: string): T | undefined {
  try {
    const data = window.localStorage.getItem(key);
    return data ? (JSON.parse(data) as T) : undefined;
  } catch (err) {
    console.error(err);
  }
}

export function removeItem(key: string) {
  try {
    window.localStorage.removeItem(key);
  } catch (err) {
    console.error(err);
  }
}
Enter fullscreen mode Exit fullscreen mode

useLocalStorage.ts code:

import { getItem, removeItem, setItem } from "@/utils/localStorage";
import { useState } from "react";

type DispatchAction<T> = T | ((prevState: T) => T);

export default function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState(() => {
    const data = getItem(key);
    return (data || initialValue) as T;
  });

  function handleDispatch(action: DispatchAction<T>) {
    if (typeof action === "function") {
      setValue((prevState) => {
        const newValue = (action as (prevState: T) => T)(prevState);
        setItem(key, newValue);
        return newValue;
      });
    } else {
      setValue(action);
      setItem(key, action);
    }
  }

  function clearState() {
    setValue(undefined as T);
    removeItem(key);
  }

  return [value, handleDispatch, clearState] as const;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The useLocalStorage hook is a powerful and reusable abstraction for managing state that persists across page reloads. It’s type-safe, handles errors gracefully, and offers an intuitive interface for developers. With this hook, you can easily integrate localStorage into your React applications and keep your codebase clean and maintainable.

Have you tried building your own custom hooks? Share your experiences in the comments below!

Top comments (2)

Collapse
 
bosi-programming profile image
Felipe Bosi • Edited

useLocalStorage is a basic react hook that everyone that uses react has to know, use or build someday. This is a simple yet great way to build one. The only thing that I would change is that on the localStorage.ts functions, I wouldn't console.log the error, put pass the error to the next function.

On my latest project, I started passing function results as [error, value] so the person that is using the code can decide how he wants to deal with the error. This is specially good with codes like this, that will be used everywhere in an app. In cases the error is just a nunsense, the dev can just ignore it. In cases it can break the app, he sends the log somewhere.

Collapse
 
link2twenty profile image
Andrew Bone

Where I work we have a version of this hook though we've gone about it in a slightly different way. Rather than attaching a state to specific entry we have a set of function that allow you (as the developer) to interact with localstorage directly. We even have event listeners so you can edit an entry else where in the app (or in a different tab) and you can catch that update and do whatever you need to.

You can check it out here if you like 😊