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
initialValueduring 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 -
isLoadingboolean to handle hydration and prevent flash of incorrect content - Functional Updates - Supports both direct values and updater functions like
useState
Learn More
MDN: sessionStorage
Complete documentation on the sessionStorage API
web.dev: Storage
Guide to choosing the right storage API for your use case
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.
"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!
"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:
"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
| Parameter | Type | Description |
|---|---|---|
key | string | The sessionStorage key to sync with. Should be unique across your app. |
initialValue | T | Default value when no stored value exists or during SSR. |
Returns
A tuple [value, setValue, isLoading]:
| Index | Type | Description |
|---|---|---|
0 | T | The current stored value |
1 | (value: T | (prev: T) => T) => void | Function to update the value (also updates sessionStorage) |
2 | boolean | true during initial hydration, false after mount completes |
Comparison with useLocalStorageState
| Feature | useLocalStorageState | useSessionStorageState |
|---|---|---|
| Persistence | Forever until manually cleared | Until tab closes |
| Cross-tab | Syncs across tabs | Isolated per tab |
| Page reload | Persists | Persists |
| Close tab | Persists | Clears |
| New tab | Shared | Fresh state |
| Use case | User prefs, auth tokens | Forms, wizards, temp filters |
Best Practices
- Use for temporary state - Only store data that should be forgotten when the user closes the tab
- Handle loading state - Always show a loading indicator while
isLoadingis true to prevent hydration mismatches - Consider the tab lifecycle - Remember that each tab has its own isolated storage
- 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;