Fiber UI LogoFiberUI

useSessionStorageState

A React hook that syncs state with sessionStorage

A React hook that persists state to sessionStorage, keeping data for the duration of the browser session. Unlike localStorage, the data is automatically cleared when the tab is closed, making it ideal for temporary state that shouldn't persist permanently.

Source Code

View the full hook implementation in the Hook Source Code section below.

Features

  • SSR Safe - Returns initialValue during server-side rendering, hydrates on mount
  • Type Safe - Full TypeScript support with generics for type inference
  • Session Scoped - Data persists only for the current tab session
  • Error Handling - Graceful fallback with console warnings on storage errors
  • Loading State - isLoading boolean to handle hydration and prevent flash of incorrect content
  • Functional Updates - Supports both direct values and updater functions like useState

Learn More

When to Use

Use useSessionStorageState when you want to:

  • Preserve form data during navigation - User fills a form, navigates away, comes back - data is still there
  • Store wizard/checkout progress - Multi-step flows that should reset on new visits
  • Keep filter/sort preferences - Temporary UI state for the current browsing session
  • Cache expensive computations - Results that are valid for the session but should refresh on new visits

Key Difference from localStorage

Unlike localStorage, sessionStorage is not shared across tabs. Each tab has its own isolated storage, and data is cleared when the tab closes.


Basic Usage

The simplest way to use useSessionStorageState - preserving form data across page navigations:

Try it out!

Fill in the form, then navigate to another page and come back - your data will still be there! But if you close the tab, it resets.

Loading...
"use client";

import { useSessionStorageState } from "@repo/hooks/storage/use-session-storage-state";

/* BASIC USAGE - Form Data Persistence */
export const Example1 = () => {
    const [formData, setFormData, isLoading] = useSessionStorageState(
        "contact-form",
        {
            name: "",
            email: "",
            message: "",
        },
    );

    const handleChange = (field: string, value: string) => {
        setFormData((prev) => ({ ...prev, [field]: value }));
    };

    if (isLoading) {
        return (
            <span className="text-muted-foreground text-sm">Loading...</span>
        );
    }

    return (
        <div className="flex w-full max-w-sm flex-col gap-3">
            <input
                className="bg-background rounded-md border px-3 py-2 text-sm"
                value={formData.name}
                onChange={(e) => handleChange("name", e.target.value)}
                placeholder="Name"
            />
            <input
                className="bg-background rounded-md border px-3 py-2 text-sm"
                value={formData.email}
                onChange={(e) => handleChange("email", e.target.value)}
                placeholder="Email"
            />
            <textarea
                className="bg-background rounded-md border px-3 py-2 text-sm"
                value={formData.message}
                onChange={(e) => handleChange("message", e.target.value)}
                placeholder="Message"
                rows={3}
            />
            <p className="text-muted-foreground text-xs">
                Form data is preserved during your session
            </p>
        </div>
    );
};

Multi-step Wizard

Perfect for preserving wizard or checkout flow progress. Users can navigate away and return without losing their place:

Try it out!

Click through the steps, then navigate away and come back - your progress is preserved for this session!

Loading...
"use client";

import { useSessionStorageState } from "@repo/hooks/storage/use-session-storage-state";

type WizardStep = "personal" | "address" | "review";

/* MULTI-STEP WIZARD - Preserving Progress */
export const Example2 = () => {
    const [currentStep, setCurrentStep, isLoading] =
        useSessionStorageState<WizardStep>("wizard-step", "personal");

    const steps: { key: WizardStep; label: string }[] = [
        { key: "personal", label: "Personal" },
        { key: "address", label: "Address" },
        { key: "review", label: "Review" },
    ];

    if (isLoading) {
        return (
            <span className="text-muted-foreground text-sm">Loading...</span>
        );
    }

    return (
        <div className="flex flex-col gap-4">
            <div className="flex gap-2">
                {steps.map((step, index) => (
                    <button
                        key={step.key}
                        className={`rounded-md px-4 py-2 text-sm ${
                            currentStep === step.key
                                ? "bg-primary text-primary-foreground"
                                : "bg-muted text-muted-foreground"
                        }`}
                        onClick={() => setCurrentStep(step.key)}
                    >
                        {index + 1}. {step.label}
                    </button>
                ))}
            </div>
            <p className="text-muted-foreground text-sm">
                Current step: <span className="font-medium">{currentStep}</span>
            </p>
            <p className="text-muted-foreground text-xs">
                Progress is preserved until you close this tab
            </p>
        </div>
    );
};

Session Filters

Store temporary filter and sort state that resets when the user starts a new browsing session:

Loading...
"use client";

import { useSessionStorageState } from "@repo/hooks/storage/use-session-storage-state";

interface FilterState {
    search: string;
    category: string;
    sortBy: "date" | "name" | "price";
}

/* SESSION FILTERS - Temporary Filter State */
export const Example3 = () => {
    const [filters, setFilters, isLoading] =
        useSessionStorageState<FilterState>("product-filters", {
            search: "",
            category: "all",
            sortBy: "date",
        });

    const updateFilter = <K extends keyof FilterState>(
        key: K,
        value: FilterState[K],
    ) => {
        setFilters((prev) => ({ ...prev, [key]: value }));
    };

    const resetFilters = () => {
        setFilters({
            search: "",
            category: "all",
            sortBy: "date",
        });
    };

    if (isLoading) {
        return (
            <span className="text-muted-foreground text-sm">Loading...</span>
        );
    }

    return (
        <div className="flex w-full max-w-md flex-col gap-3">
            <input
                className="bg-background rounded-md border px-3 py-2 text-sm"
                value={filters.search}
                onChange={(e) => updateFilter("search", e.target.value)}
                placeholder="Search..."
            />
            <div className="flex gap-2">
                <select
                    className="bg-background flex-1 rounded-md border px-3 py-2 text-sm"
                    value={filters.category}
                    onChange={(e) => updateFilter("category", e.target.value)}
                >
                    <option value="all">All Categories</option>
                    <option value="electronics">Electronics</option>
                    <option value="clothing">Clothing</option>
                </select>
                <select
                    className="bg-background flex-1 rounded-md border px-3 py-2 text-sm"
                    value={filters.sortBy}
                    onChange={(e) =>
                        updateFilter(
                            "sortBy",
                            e.target.value as FilterState["sortBy"],
                        )
                    }
                >
                    <option value="date">Sort by Date</option>
                    <option value="name">Sort by Name</option>
                    <option value="price">Sort by Price</option>
                </select>
            </div>
            <button
                className="bg-muted text-muted-foreground rounded-md px-3 py-2 text-sm"
                onClick={resetFilters}
            >
                Reset Filters
            </button>
        </div>
    );
};

Handling the Loading State

Just like useLocalStorageState, the isLoading parameter helps prevent hydration mismatches in SSR applications:

const [step, setStep, isLoading] = useSessionStorageState("wizard-step", 1);

if (isLoading) {
    return <Skeleton />; // Prevent flash of wrong step
}

return <WizardStep step={step} />;

API Reference

function useSessionStorageState<T>(
    key: string,
    initialValue: T,
): [T, (value: T | ((prev: T) => T)) => void, boolean];

Parameters

ParameterTypeDescription
keystringThe sessionStorage key to sync with. Should be unique across your app.
initialValueTDefault value when no stored value exists or during SSR.

Returns

A tuple [value, setValue, isLoading]:

IndexTypeDescription
0TThe current stored value
1(value: T | (prev: T) => T) => voidFunction to update the value (also updates sessionStorage)
2booleantrue during initial hydration, false after mount completes

Comparison with useLocalStorageState

FeatureuseLocalStorageStateuseSessionStorageState
PersistenceForever until manually clearedUntil tab closes
Cross-tabSyncs across tabsIsolated per tab
Page reloadPersistsPersists
Close tabPersistsClears
New tabSharedFresh state
Use caseUser prefs, auth tokensForms, wizards, temp filters

Best Practices

  1. Use for temporary state - Only store data that should be forgotten when the user closes the tab
  2. Handle loading state - Always show a loading indicator while isLoading is true to prevent hydration mismatches
  3. Consider the tab lifecycle - Remember that each tab has its own isolated storage
  4. Use unique keys - Prefix keys to avoid conflicts: "checkout-step", "product-filters"

Hook Source Code

import { useState, useEffect, useCallback } from "react";

type SetValue<T> = T | ((prevValue: T) => T);

/**
 * Return type for useSessionStorageState hook
 */
export type UseSessionStorageStateReturn<T> = [
    T,
    (value: SetValue<T>) => void,
    boolean,
];

/**
 * A React hook that syncs state with sessionStorage.
 * - SSR safe: Returns initialValue during server-side rendering
 * - Type-safe: Accepts generic type parameter
 * - Handles JSON serialization/deserialization automatically
 *
 * Note: Unlike localStorage, sessionStorage is not shared across tabs,
 * so this hook does not include cross-tab synchronization.
 *
 * @param key - The sessionStorage key to sync with
 * @param initialValue - The initial value to use if no stored value exists
 * @returns A tuple of [value, setValue, isLoading]
 */
export function useSessionStorageState<T>(
    key: string,
    initialValue: T,
): UseSessionStorageStateReturn<T> {
    const [isLoading, setIsLoading] = useState(true);
    const [storedValue, setStoredValue] = useState<T>(initialValue);

    // Hydrate from sessionStorage on mount
    useEffect(() => {
        try {
            const item = sessionStorage.getItem(key);
            if (item !== null) {
                setStoredValue(JSON.parse(item) as T);
            }
        } catch (error) {
            console.warn(`Error reading sessionStorage key "${key}":`, error);
        } finally {
            setIsLoading(false);
        }
    }, [key]);

    // Update sessionStorage when value changes
    const setValue = useCallback(
        (value: SetValue<T>) => {
            try {
                setStoredValue((prev) => {
                    const valueToStore =
                        value instanceof Function ? value(prev) : value;

                    if (typeof window !== "undefined") {
                        sessionStorage.setItem(
                            key,
                            JSON.stringify(valueToStore),
                        );
                    }

                    return valueToStore;
                });
            } catch (error) {
                console.warn(
                    `Error setting sessionStorage key "${key}":`,
                    error,
                );
            }
        },
        [key],
    );

    return [storedValue, setValue, isLoading];
}

export default useSessionStorageState;