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

@piyawasin/use-persisted-state

v1.0.5

Published

A tiny, fully React-compatible hook for persisting state to localStorage, sessionStorage, or custom storage. Matches useState signature exactly with support for functional updates. Works with React Native, supports namespacing, and includes SSR-safe fallb

Downloads

29

Readme

use-persisted-state

A tiny, flexible React hook for persisting state to localStorage, sessionStorage, or custom storage providers. Can be used with React Native via the custom storage option. Supports namespacing, pluggable storage, and SSR-safe fallbacks.

Features

  • 100% React-compatible: Matches useState signature exactly, including support for functional updates.
  • Fully typed with TypeScript for a seamless developer experience.
  • Persist React state to localStorage (default), sessionStorage, or any custom storage provider.
  • Safe for server-side rendering (SSR): automatically falls back to noop storage when not in a browser.
  • Easily customize key namespacing and serialization/deserialization as needed.
  • Advanced: Plug in your own storage provider for testing or non-standard environments.

Installation

npm install @piyawasin/use-persisted-state

Usage

Basic Example: Theme Switcher

import { usePersistedState } from "@piyawasin/use-persisted-state";

function ThemeSwitcher() {
  const [theme, setTheme] = usePersistedState("theme", "light");
  // Persists to key: "persist:(default):theme"
  return (
    <button
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
      style={{ padding: 8 }}
    >
      Switch to {theme === "light" ? "dark" : "light"} mode
    </button>
  );
}

Functional Updates (Just Like useState)

The hook supports both direct values and functional updates, exactly like React's useState:

import { usePersistedState } from "@piyawasin/use-persisted-state";

function Counter() {
  const [count, setCount] = usePersistedState("counter", 0);
  
  return (
    <div>
      <p>Count: {count}</p>
      
      {/* Direct value updates */}
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(count + 1)}>Increment (direct)</button>
      
      {/* Functional updates - safer for concurrent updates */}
      <button onClick={() => setCount(prev => prev + 1)}>Increment</button>
      <button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
      <button onClick={() => setCount(c => c * 2)}>Double</button>
    </div>
  );
}

With custom namespace

import { usePersistedState } from "@piyawasin/use-persisted-state";

function ThemeSwitcherWithNamespace() {
  const [theme, setTheme] = usePersistedState("theme", "light", {
    namespace: "settings",
  });
  // Persists to key: "persist:settings:theme"
  return (
    <button
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
      style={{ padding: 8 }}
    >
      Switch to {theme === "light" ? "dark" : "light"} mode
    </button>
  );
}

With sessionStorage

import { usePersistedState } from "@piyawasin/use-persisted-state";

function ThemeSwitcherSession() {
  const [theme, setTheme] = usePersistedState("theme", "light", {
    storage: window.sessionStorage,
  });
  return (
    <button
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
      style={{ padding: 8 }}
    >
      Switch to {theme === "light" ? "dark" : "light"} mode
    </button>
  );
}

With custom storage provider

import {
  usePersistedState,
  type StorageProviderInterface,
} from "@piyawasin/use-persisted-state";

const memoryStorage: StorageProviderInterface = {
  store: {} as Record<string, string>,
  getItem(key) {
    return this.store[key] ?? null;
  },
  setItem(key, value) {
    this.store[key] = value;
  },
  removeItem(key) {
    delete this.store[key];
  },
};

function ThemeSwitcherCustomStorage() {
  const [theme, setTheme] = usePersistedState("theme", "light", {
    storage: memoryStorage,
  });
  return (
    <button
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
      style={{ padding: 8 }}
    >
      Switch to {theme === "light" ? "dark" : "light"} mode
    </button>
  );
}

React Native Example (AsyncStorage)

You can use this hook in React Native by providing a custom storage provider based on @react-native-async-storage/async-storage:

import {
  usePersistedState,
  type StorageProviderInterface,
} from "@piyawasin/use-persisted-state";
import AsyncStorage from "@react-native-async-storage/async-storage";

const asyncStorageProvider: StorageProviderInterface = {
  async getItem(key) {
    return await AsyncStorage.getItem(key);
  },
  async setItem(key, value) {
    await AsyncStorage.setItem(key, value);
  },
  async removeItem(key) {
    await AsyncStorage.removeItem(key);
  },
};

function ThemeSwitcherNative() {
  const [theme, setTheme] = usePersistedState("theme", "light", {
    storage: asyncStorageProvider,
  });
  return (
    <Button
      title={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
      onPress={() => setTheme(theme === "light" ? "dark" : "light")}
    />
  );
}

Note

  • If you use an async storage provider (like AsyncStorage), you may need to handle asynchronous state initialization and updates in your component logic.
  • This package is compatible with React Native as long as you provide a compatible storage provider.

API

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

  • key: string — The key to persist under.
  • initialValue: T — The initial state value.
  • options?: { namespace?: string; storage?: StorageProviderInterface; serialize?: (value: T) => string; deserialize?: (raw: string) => T; }
    • namespace — Optional prefix for the key. Defaults to "(default)"
    • storage — Optional storage provider. Defaults to localStorage (if available).
    • serialize — Optional custom serializer function. Defaults to JSON.stringify.
    • deserialize — Optional custom deserializer function. Defaults to JSON.parse.

Returns [state, setState]exactly like useState. The setState function accepts both direct values (T) and updater functions ((prevState: T) => T).

StorageProviderInterface

A minimal interface for storage providers:

interface StorageProviderInterface {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
  removeItem(key: string): void;
}

SSR / Non-browser environments

If no valid storage is available, the hook logs a warning and falls back to a no-op provider (nothing is persisted).

Comparison to redux-persist

While libraries like redux-persist are great for persisting entire Redux stores, sometimes you just want to persist a handful of values—like a theme, a language preference, or a form draft—without the complexity of global state management. usePersistedState is designed for these cases: it's intuitive, minimal, and lets you persist state with the same API as useState.

Serialization

Important: All data passed to usePersistedState must be serializable by JSON.stringify and deserializable by JSON.parse. This means:

  • Do not use functions, symbols, or non-serializable objects as state.
  • Most plain objects, arrays, numbers, strings, and booleans are supported.
  • If you need to persist more complex data, you can provide a custom serialization/deserialization mechanism using the serialize and deserialize options.

Custom Serialization Example

If you want to persist data that is not directly supported by JSON (such as Date, Map, Set, or custom classes), you can pass custom serialize and deserialize functions to the hook:

import usePersistedState from "@piyawasin/use-persisted-state";

// Example: Persisting a Date object
const [date, setDate] = usePersistedState<Date>("my-date", new Date(), {
  serialize: (value) => value.toISOString(),
  deserialize: (raw) => new Date(raw),
});

// Example: Persisting a Map
const [map, setMap] = usePersistedState<Map<string, number>>(
  "my-map",
  new Map(),
  {
    serialize: (value) => JSON.stringify([...value.entries()]),
    deserialize: (raw) => new Map(JSON.parse(raw)),
  }
);

Higher-Order Hook: Pre-configuring usePersistedState

You can create a custom version of usePersistedState with certain options (like namespace, serialize, or deserialize) pre-filled. This is similar to a higher-order component (HOC) pattern, but for hooks. It allows you to DRY up your code and avoid repeating options everywhere.

Example: Factory for Namespaced State

import usePersistedState from "@piyawasin/use-persisted-state";

function createNamespacedPersistedState(namespace: string) {
  return function useNamespacedPersistedState<T>(
    key: string,
    initialValue: T,
    options = {}
  ) {
    return usePersistedState(key, initialValue, { ...options, namespace });
  };
}

// Usage
const useSettingsState = createNamespacedPersistedState("settings");
const [theme, setTheme] = useSettingsState("theme", "light");

Example: Factory with Custom Serialization

import usePersistedState from "@piyawasin/use-persisted-state";

function createCustomSerializedState<T>(
  serialize: (value: T) => string,
  deserialize: (raw: string) => T
) {
  return function useCustomSerializedState(
    key: string,
    initialValue: T,
    options = {}
  ) {
    return usePersistedState(key, initialValue, {
      ...options,
      serialize,
      deserialize,
    });
  };
}

// Usage for Dates
const useDateState = createCustomSerializedState<Date>(
  (date) => date.toISOString(),
  (raw) => new Date(raw)
);
const [date, setDate] = useDateState("my-date", new Date());

This pattern is especially useful if you have multiple pieces of state that should share the same namespace or serialization logic.

Disclaimer

This library, originally developed for personal use, is being distributed on an "as-is" basis. The creator makes no warranties or guarantees regarding its performance, functionality, or suitability for any specific application.

License

MIT