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-session-storage

v0.0.31

Published

A React hook for persisting state in sessionStorage

Readme


Overview

@usefy/use-session-storage provides a useState-like API for persisting data in sessionStorage. Features include same-tab component synchronization, custom serialization, lazy initialization, and error handling. Data persists during the browser session (tab lifetime) but clears when the tab is closed. Each tab has isolated storage, making it perfect for temporary form data, wizard steps, and session-specific state.

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

Why use-session-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
  • Tab Isolation — Each browser tab has its own session storage
  • React 18+ Optimized — Built with useSyncExternalStore for Concurrent Mode compatibility
  • Auto-Cleanup — Data cleared automatically when tab closes
  • 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

localStorage vs sessionStorage

| Feature | localStorage | sessionStorage | | ---------------- | ------------------------ | ------------------------- | | Data persistence | Until explicitly cleared | Until tab closes | | Tab sharing | Shared across all tabs | Isolated per tab | | Best for | User preferences, themes | Form drafts, wizard steps |


Installation

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

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

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

Peer Dependencies

This package requires React 18 or 19:

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

Quick Start

import { useSessionStorage } from "@usefy/use-session-storage";

function CheckoutForm() {
  const [formData, setFormData, clearForm] = useSessionStorage(
    "checkout-form",
    {
      name: "",
      email: "",
      address: "",
    }
  );

  return (
    <form>
      <input
        value={formData.name}
        onChange={(e) =>
          setFormData((prev) => ({ ...prev, name: e.target.value }))
        }
        placeholder="Name"
      />
      <input
        value={formData.email}
        onChange={(e) =>
          setFormData((prev) => ({ ...prev, email: e.target.value }))
        }
        placeholder="Email"
      />
      <button type="button" onClick={clearForm}>
        Clear Form
      </button>
    </form>
  );
}

API Reference

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

A hook that persists state in sessionStorage for the duration of the browser session.

Parameters

| Parameter | Type | Description | | -------------- | ----------------------------- | ------------------------------------------ | | key | string | The sessionStorage key | | initialValue | T \| () => T | Initial value or lazy initializer function | | options | UseSessionStorageOptions<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 | | 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

Same-Tab Component Synchronization

import { useSessionStorage } from "@usefy/use-session-storage";

// Multiple components using the same key automatically stay in sync!
function WizardProgress() {
  const [step] = useSessionStorage("wizard-step", 1);
  return <ProgressBar current={step} total={5} />;
}

function WizardNavigation() {
  const [step, setStep] = useSessionStorage("wizard-step", 1);

  return (
    <div>
      <button onClick={() => setStep((s) => s - 1)} disabled={step === 1}>
        Back
      </button>
      <button onClick={() => setStep((s) => s + 1)} disabled={step === 5}>
        Next {/* WizardProgress automatically updates! */}
      </button>
    </div>
  );
}

function WizardContent() {
  const [step] = useSessionStorage("wizard-step", 1);
  // Also updates when WizardNavigation changes step!
  return <StepContent step={step} />;
}

Multi-Step Wizard

import { useSessionStorage } from "@usefy/use-session-storage";

function SignupWizard() {
  const [step, setStep] = useSessionStorage("signup-step", 1);
  const [formData, setFormData, resetForm] = useSessionStorage("signup-data", {
    email: "",
    password: "",
    profile: {},
  });

  const handleNext = () => setStep((prev) => prev + 1);
  const handleBack = () => setStep((prev) => prev - 1);

  const handleComplete = async () => {
    await submitSignup(formData);
    resetForm();
    setStep(1);
  };

  return (
    <div>
      <p>Step {step} of 3</p>

      {step === 1 && (
        <EmailStep
          value={formData.email}
          onChange={(email) => setFormData((prev) => ({ ...prev, email }))}
          onNext={handleNext}
        />
      )}

      {step === 2 && (
        <PasswordStep
          value={formData.password}
          onChange={(password) =>
            setFormData((prev) => ({ ...prev, password }))
          }
          onBack={handleBack}
          onNext={handleNext}
        />
      )}

      {step === 3 && (
        <ProfileStep
          value={formData.profile}
          onChange={(profile) => setFormData((prev) => ({ ...prev, profile }))}
          onBack={handleBack}
          onComplete={handleComplete}
        />
      )}
    </div>
  );
}

Form Draft (Auto-Restore)

import { useSessionStorage } from "@usefy/use-session-storage";

function ContactForm() {
  const [draft, setDraft, clearDraft] = useSessionStorage("contact-draft", {
    subject: "",
    message: "",
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await sendMessage(draft);
    clearDraft(); // Clear after successful submit
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={draft.subject}
        onChange={(e) =>
          setDraft((prev) => ({ ...prev, subject: e.target.value }))
        }
        placeholder="Subject"
      />
      <textarea
        value={draft.message}
        onChange={(e) =>
          setDraft((prev) => ({ ...prev, message: e.target.value }))
        }
        placeholder="Message"
      />
      <p className="hint">Your draft is auto-saved in this tab</p>
      <button type="submit">Send</button>
      <button type="button" onClick={clearDraft}>
        Discard
      </button>
    </form>
  );
}

Shopping Cart (Per-Tab)

import { useSessionStorage } from "@usefy/use-session-storage";

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

function TabCart() {
  const [cart, setCart, clearCart] = useSessionStorage<CartItem[]>(
    "tab-cart",
    []
  );

  const addItem = (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, { id: product.id, name: product.name, quantity: 1 }];
    });
  };

  return (
    <div>
      <p>Cart items: {cart.length}</p>
      <p className="hint">This cart is specific to this tab only</p>
      <button onClick={clearCart}>Clear Cart</button>
    </div>
  );
}

Temporary Auth Token

import { useSessionStorage } from "@usefy/use-session-storage";

function ProtectedPage() {
  const [token, setToken, clearToken] = useSessionStorage<string | null>(
    "auth-token",
    null
  );

  const login = async (credentials: Credentials) => {
    const response = await authenticate(credentials);
    setToken(response.token);
  };

  const logout = () => {
    clearToken();
    // Token is automatically cleared when tab closes
  };

  if (!token) {
    return <LoginForm onLogin={login} />;
  }

  return (
    <div>
      <p>You are logged in (this session only)</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Custom Serialization (Date)

import { useSessionStorage } from "@usefy/use-session-storage";

function SessionTimer() {
  const [sessionStart] = useSessionStorage<Date>("session-start", new Date(), {
    serializer: (date) => date.toISOString(),
    deserializer: (str) => new Date(str),
  });

  const [elapsed, setElapsed] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setElapsed(Math.floor((Date.now() - sessionStart.getTime()) / 1000));
    }, 1000);
    return () => clearInterval(interval);
  }, [sessionStart]);

  return <div>Session duration: {elapsed} seconds</div>;
}

Error Handling

import { useSessionStorage } from "@usefy/use-session-storage";

function RobustSessionStorage() {
  const [data, setData] = useSessionStorage(
    "session-data",
    { items: [] },
    {
      onError: (error) => {
        console.error("Session storage error:", error.message);
        toast.error("Failed to save session data");
      },
    }
  );

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

Lazy Initialization

import { useSessionStorage } from "@usefy/use-session-storage";

function ExpensiveDefaultDemo() {
  // Expensive computation only runs if no stored value exists
  const [cache, setCache] = useSessionStorage("session-cache", () => {
    console.log("Building initial cache...");
    return buildExpensiveCache();
  });

  return <CacheViewer cache={cache} />;
}

Quiz Progress

import { useSessionStorage } from "@usefy/use-session-storage";

interface QuizState {
  currentQuestion: number;
  answers: Record<number, string>;
  startTime: number;
}

function Quiz() {
  const [quiz, setQuiz, resetQuiz] = useSessionStorage<QuizState>(
    "quiz-progress",
    {
      currentQuestion: 0,
      answers: {},
      startTime: Date.now(),
    }
  );

  const submitAnswer = (answer: string) => {
    setQuiz((prev) => ({
      ...prev,
      answers: { ...prev.answers, [prev.currentQuestion]: answer },
      currentQuestion: prev.currentQuestion + 1,
    }));
  };

  const handleComplete = async () => {
    await submitQuiz(quiz.answers);
    resetQuiz();
  };

  return (
    <div>
      <p>Question {quiz.currentQuestion + 1} of 10</p>
      <QuestionCard
        question={questions[quiz.currentQuestion]}
        onAnswer={submitAnswer}
      />
      <button onClick={resetQuiz}>Restart Quiz</button>
    </div>
  );
}

TypeScript

This hook is written in TypeScript with full generic support.

import {
  useSessionStorage,
  type UseSessionStorageOptions,
  type UseSessionStorageReturn,
  type InitialValue,
} from "@usefy/use-session-storage";

// Generic type inference
const [name, setName] = useSessionStorage("name", "Guest"); // string
const [step, setStep] = useSessionStorage("step", 1); // number
const [items, setItems] = useSessionStorage("items", ["a"]); // string[]

// Explicit generic type
interface FormData {
  email: string;
  message: string;
}
const [form, setForm] = useSessionStorage<FormData>("form", {
  email: "",
  message: "",
});

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 sessionStorage is empty
  • Return stored value when sessionStorage has data
  • Support lazy initialization with function
  • Not call initializer when sessionStorage has data
  • Fallback to initial value when JSON parse fails
  • Call onError when sessionStorage 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 and sessionStorage
  • Support functional updates
  • Handle object values
  • Handle array values
  • Call onError when write fails

License

MIT © mirunamu

This package is part of the usefy monorepo.