← articleshooks unleashed

useLocalStorage: persist React state in localStorage

A small custom hook that reads and writes JSON to localStorage, with lazy initialization on first render.

Persisting state across reloads usually means reading from localStorage on mount and writing back on every update. This hook wraps that pattern.

The hook

/* useLocalStorage */

import { useState } from "react";

const useLocalStorage = (key, initialValue) => {
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      alert("Something went wrong.");
      console.log(error);    
    }
  });

  const setLocalValue = (value) => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
      setValue(value)
    } catch (error) {
      alert("Something went wrong.");
      console.log(error);   
    }
  }

  return [value, setLocalValue];
}

export default useLocalStorage;

How it works

The hook takes a key and an initialValue. On first render, it tries to read and parse JSON from localStorage. If nothing is stored (or parsing fails), it falls back to initialValue.

setLocalValue writes the new value to localStorage and updates React state in one step. The return shape mirrors useState: [value, setLocalValue].

Example: theme toggle

import React from 'react';
import useLocalStorage from './useLocalStorage';

const App = () => {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
  }

  return (
    <div className={`app ${theme}`}>
      <h1>Theme persists across reloads</h1>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle theme</button>
    </div>
  );
}

export default App;

The selected theme survives a refresh because it's stored under the 'theme' key.

Notes

  • Wrap reads/writes in try/catch if you expect quota errors or private browsing restrictions.
  • For SSR, guard window.localStorage — it doesn't exist on the server.
  • If you need cross-tab sync, listen for the storage event and update state when another tab writes the same key.