Managing Local Storage in React with useLocalStorage Hook

Saiful Islam - Jan 26 - - Dev Community

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!

. . . . . . . . . . . . . . .