Build a useLocalStorage Hook in React

Build a custom Hook that reads and writes state to localStorage, keeps values across reloads, and handles JSON and browser-only access.

6 min read

A useLocalStorage Hook keeps a piece of React state in sync with the browser's localStorage, so the value survives a page reload. It reads the stored value on first render and writes it back whenever state changes.

Use it for a theme, a saved form draft, or any preference the user expects to persist. When the value changes, the Hook writes it back so state and storage never drift.

The Hook in three parts

The Hook combines a lazy state read, a write Effect, and JSON conversion. localStorage stores only strings, so objects and numbers must be converted on the way in and out.

App.jsxApp.jsx
import { useEffect, useState } from "react";
 
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored !== null ? JSON.parse(stored) : initialValue;
  });
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);
  return [value, setValue];
}

Passing a function to useState runs it only during the first render, so the Hook reads localStorage once instead of on every render. getItem returns null when the key is missing, and the Hook falls back to the initial value.

The Effect runs after every committed render where value or key changed, writing the latest value back. It needs no cleanup because setItem does not create a subscription.

Use the Hook for a theme toggle

App.jsxApp.jsx
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage("theme", "light");
  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }
  return <button onClick={toggleTheme}>Theme: {theme}</button>;
}

On the first visit the key is missing, so the Hook starts with light. Clicking the button stores dark, and after a reload the Hook reads dark again instead of resetting.

Stored values are strings

localStorage stores keys and values as strings. Call JSON.stringify before setItem and JSON.parse after getItem when you store objects or numbers.

Handle errors and server rendering

JSON.parse throws when the stored value is malformed, and localStorage does not exist during server rendering. The version above crashes in both cases, so add two guards.

App.jsxApp.jsx
const [value, setValue] = useState(() => {
  if (typeof window === "undefined") {
    return initialValue;
  }
  try {
    const stored = localStorage.getItem(key);
    return stored !== null ? JSON.parse(stored) : initialValue;
  } catch {
    return initialValue;
  }
});

The window check keeps the first render safe on the server, before returning the initial value instead of touching localStorage. The try and catch block falls back to the initial value when a stored entry is not valid JSON, so one corrupted key cannot crash the component during render.

The server check matters most in a framework that renders React on the server, such as Next.js. A component using this Hook can render there without throwing, then pick up the real stored value once the Effect runs in the browser after hydration.

What localStorage can and cannot do

  • Values persist across page reloads and browser restarts.
  • Data is scoped to one origin, and private mode clears it.
  • It is synchronous and suits small values, not large files.
  • Writes do not notify other tabs unless you listen for the storage event.

For the extraction steps, see how to create a custom Hook, and for the timing rules see when you actually need useEffect.

Rune AI

Rune AI

Key Insights

  • Read the initial value with a lazy useState initializer.
  • Write changes back through an Effect keyed on value and key.
  • Store values as JSON with stringify and parse.
  • Guard window access for server rendering.
  • Wrap parsing in try and catch for malformed data.
RunePowered by Rune AI

Frequently Asked Questions

Why use a lazy initializer with useState?

Passing a function to useState runs it only during the first render. This avoids reading localStorage on every render and keeps the Hook safe during server rendering.

What happens if the stored value is malformed JSON?

JSON.parse throws an error. Wrap the read in a try and catch block and fall back to the initial value so the component still renders.

Does localStorage work during server rendering?

No. localStorage is a browser-only API, so guard access with a window check or return the initial value when window is undefined.

Conclusion

A useLocalStorage Hook reads its initial value lazily from localStorage and writes it back through an Effect whenever it changes. Store values as JSON and guard window access so the Hook stays safe in the browser and on the server.