npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@usefy/use-local-storage

v0.0.31

Published

A React hook for persisting state in localStorage with automatic synchronization

Readme


Overview

@usefy/use-local-storage provides a useState-like API for persisting data in localStorage. Features include same-tab component synchronization, cross-tab synchronization, custom serialization, lazy initialization, error handling, and a removeValue function. Data persists across browser sessions.

Part of the @usefy ecosystem — a collection of production-ready React hooks designed for modern applications.

Why use-local-storage?

  • Zero Dependencies — Pure React implementation with no external dependencies
  • TypeScript First — Full type safety with generics and exported interfaces
  • useState-like API — Familiar tuple return: [value, setValue, removeValue]
  • Same-Tab Sync — Multiple components using the same key stay in sync automatically
  • Cross-Tab Sync — Automatic synchronization across browser tabs
  • React 18+ Optimized — Built with useSyncExternalStore for Concurrent Mode compatibility
  • Custom Serialization — Support for Date, Map, Set, or any custom type
  • Lazy Initialization — Function initializer support for expensive defaults
  • Error HandlingonError callback for graceful error recovery
  • SSR Compatible — Works seamlessly with Next.js, Remix, and other SSR frameworks
  • Stable References — Memoized functions for optimal performance
  • Well Tested — Comprehensive test coverage with Vitest

Installation

# npm
npm install @usefy/use-local-storage

# yarn
yarn add @usefy/use-local-storage

# pnpm
pnpm add @usefy/use-local-storage

Peer Dependencies

This package requires React 18 or 19:

{
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0"
  }
}

Quick Start

import { useLocalStorage } from "@usefy/use-local-storage";

function ThemeToggle() {
  const [theme, setTheme, removeTheme] = useLocalStorage("theme", "light");

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme("dark")}>Dark</button>
      <button onClick={() => setTheme("light")}>Light</button>
      <button onClick={removeTheme}>Reset</button>
    </div>
  );
}

API Reference

useLocalStorage<T>(key, initialValue, options?)

A hook that persists state in localStorage with automatic synchronization.

Parameters

| Parameter | Type | Description | | -------------- | --------------------------- | ------------------------------------------ | | key | string | The localStorage key | | initialValue | T \| () => T | Initial value or lazy initializer function | | options | UseLocalStorageOptions<T> | Configuration options |

Options

| Option | Type | Default | Description | | -------------- | ------------------------ | ---------------- | ------------------------------ | | serializer | (value: T) => string | JSON.stringify | Custom serializer function | | deserializer | (value: string) => T | JSON.parse | Custom deserializer function | | syncTabs | boolean | true | Sync value across browser tabs | | onError | (error: Error) => void | — | Callback for error handling |

Returns [T, SetValue<T>, RemoveValue]

| Index | Type | Description | | ----- | ----------------------------- | --------------------------------------------- | | [0] | T | Current stored value | | [1] | Dispatch<SetStateAction<T>> | Function to update value (same as useState) | | [2] | () => void | Function to remove value and reset to initial |


Examples

Basic String Storage

import { useLocalStorage } from "@usefy/use-local-storage";

function NameInput() {
  const [name, setName, removeName] = useLocalStorage("userName", "");

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter your name"
      />
      <button onClick={removeName}>Clear</button>
      <p>Hello, {name || "Guest"}!</p>
    </div>
  );
}

Object State

import { useLocalStorage } from "@usefy/use-local-storage";

interface UserSettings {
  notifications: boolean;
  language: string;
  fontSize: number;
}

function SettingsPanel() {
  const [settings, setSettings] = useLocalStorage<UserSettings>("settings", {
    notifications: true,
    language: "en",
    fontSize: 16,
  });

  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={settings.notifications}
          onChange={(e) =>
            setSettings((prev) => ({
              ...prev,
              notifications: e.target.checked,
            }))
          }
        />
        Enable Notifications
      </label>

      <select
        value={settings.language}
        onChange={(e) =>
          setSettings((prev) => ({ ...prev, language: e.target.value }))
        }
      >
        <option value="en">English</option>
        <option value="ko">한국어</option>
        <option value="ja">日本語</option>
      </select>

      <input
        type="range"
        min="12"
        max="24"
        value={settings.fontSize}
        onChange={(e) =>
          setSettings((prev) => ({ ...prev, fontSize: +e.target.value }))
        }
      />
    </div>
  );
}

Lazy Initialization

import { useLocalStorage } from "@usefy/use-local-storage";

function ExpensiveDefaultDemo() {
  // Expensive computation only runs if no stored value exists
  const [config, setConfig] = useLocalStorage("appConfig", () => {
    console.log("Computing expensive default...");
    return generateComplexDefaultConfig();
  });

  return <ConfigEditor config={config} onChange={setConfig} />;
}

Custom Serialization (Date)

import { useLocalStorage } from "@usefy/use-local-storage";

function DatePicker() {
  const [lastVisit, setLastVisit] = useLocalStorage<Date>(
    "lastVisit",
    new Date(),
    {
      serializer: (date) => date.toISOString(),
      deserializer: (str) => new Date(str),
    }
  );

  return (
    <div>
      <p>Last visit: {lastVisit.toLocaleDateString()}</p>
      <button onClick={() => setLastVisit(new Date())}>Update</button>
    </div>
  );
}

Custom Serialization (Map)

import { useLocalStorage } from "@usefy/use-local-storage";

function FavoritesManager() {
  const [favorites, setFavorites] = useLocalStorage<Map<string, boolean>>(
    "favorites",
    new Map(),
    {
      serializer: (map) => JSON.stringify([...map.entries()]),
      deserializer: (str) => new Map(JSON.parse(str)),
    }
  );

  const toggleFavorite = (id: string) => {
    setFavorites((prev) => {
      const next = new Map(prev);
      next.has(id) ? next.delete(id) : next.set(id, true);
      return next;
    });
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name}
          <button onClick={() => toggleFavorite(item.id)}>
            {favorites.has(item.id) ? "★" : "☆"}
          </button>
        </li>
      ))}
    </ul>
  );
}

Error Handling

import { useLocalStorage } from "@usefy/use-local-storage";

function RobustStorage() {
  const [data, setData] = useLocalStorage(
    "userData",
    { items: [] },
    {
      onError: (error) => {
        console.error("Storage error:", error.message);
        // Could show toast notification, log to analytics, etc.
        toast.error("Failed to save data. Storage may be full.");
      },
    }
  );

  return <DataEditor data={data} onChange={setData} />;
}

Same-Tab Component Synchronization

import { useLocalStorage } from "@usefy/use-local-storage";

// Multiple components using the same key automatically stay in sync!
function Navbar() {
  const [user] = useLocalStorage<User | null>("user", null);
  return <span>Welcome, {user?.name || "Guest"}</span>;
}

function LoginForm() {
  const [, setUser] = useLocalStorage<User | null>("user", null);

  const handleLogin = async (credentials: Credentials) => {
    const user = await authenticate(credentials);
    setUser(user); // Navbar automatically updates!
  };

  return <button onClick={() => handleLogin(creds)}>Login</button>;
}

function Sidebar() {
  const [user] = useLocalStorage<User | null>("user", null);
  // Also updates when LoginForm calls setUser!
  return user ? <UserMenu user={user} /> : <LoginPrompt />;
}

Cross-Tab Synchronization

import { useLocalStorage } from "@usefy/use-local-storage";

function CartCounter() {
  // Changes in one tab automatically sync to other tabs
  const [cartItems, setCartItems] = useLocalStorage<string[]>("cart", []);

  const addItem = (item: string) => {
    setCartItems((prev) => [...prev, item]);
  };

  return (
    <div>
      <span>Cart: {cartItems.length} items</span>
      {/* Other tabs will see the updated count */}
    </div>
  );
}

Disable Tab Sync

import { useLocalStorage } from "@usefy/use-local-storage";

function LocalOnlyData() {
  // Don't sync changes from other tabs
  const [draft, setDraft] = useLocalStorage("draft", "", {
    syncTabs: false,
  });

  return (
    <textarea
      value={draft}
      onChange={(e) => setDraft(e.target.value)}
      placeholder="This draft won't sync with other tabs"
    />
  );
}

Shopping Cart

import { useLocalStorage } from "@usefy/use-local-storage";

interface CartItem {
  id: string;
  name: string;
  quantity: number;
  price: number;
}

function ShoppingCart() {
  const [cart, setCart, clearCart] = useLocalStorage<CartItem[]>("cart", []);

  const addToCart = (product: Product) => {
    setCart((prev) => {
      const existing = prev.find((item) => item.id === product.id);
      if (existing) {
        return prev.map((item) =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      return [...prev, { ...product, quantity: 1 }];
    });
  };

  const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);

  return (
    <div>
      <h2>Cart ({cart.length} items)</h2>
      <ul>
        {cart.map((item) => (
          <li key={item.id}>
            {item.name} x {item.quantity} - ${item.price * item.quantity}
          </li>
        ))}
      </ul>
      <p>Total: ${total.toFixed(2)}</p>
      <button onClick={clearCart}>Clear Cart</button>
    </div>
  );
}

TypeScript

This hook is written in TypeScript with full generic support.

import {
  useLocalStorage,
  type UseLocalStorageOptions,
  type UseLocalStorageReturn,
  type InitialValue,
} from "@usefy/use-local-storage";

// Generic type inference
const [name, setName] = useLocalStorage("name", "John"); // string
const [count, setCount] = useLocalStorage("count", 0); // number
const [items, setItems] = useLocalStorage("items", [1, 2]); // number[]

// Explicit generic type
interface User {
  id: string;
  name: string;
}
const [user, setUser] = useLocalStorage<User | null>("user", null);

Testing

This package maintains comprehensive test coverage to ensure reliability and stability.

Test Coverage

📊 View Detailed Coverage Report (GitHub Pages)

Test Categories

  • Return initial value when localStorage is empty
  • Return stored value when localStorage has data
  • Support lazy initialization with function
  • Not call initializer when localStorage has data
  • Fallback to initial value when JSON parse fails
  • Call onError when localStorage read fails
  • Sync ComponentB when ComponentA updates the same key
  • Sync multiple components using the same key
  • Sync when using functional updates
  • Sync when removeValue is called
  • Not affect components with different keys
  • Handle rapid updates from different components
  • Cleanup listeners on unmount
  • Update value when storage event is fired
  • Reset to initial when key is removed in another tab
  • Ignore storage events for different keys
  • Not sync when syncTabs is false
  • Fallback to initial value on invalid JSON

License

MIT © mirunamu

This package is part of the usefy monorepo.