usePersistedState
React hook to sync state with localStorage or sessionStorage.
Made by lucasName (localStorage)
Count (persisted)
0
Values survive page refreshes via localStorage.
import usePersistedState from "@/components/targetblank/hooks/use-persisted-state";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import React from "react";
export default function PersistedStateDemo() {
const [name, setName, clearName] = usePersistedState(
"demo:persisted-name",
"",
);
const [count, setCount, clearCount] = usePersistedState(
"demo:persisted-count",
0,
);
return (
<div className="flex flex-col gap-4 w-[360px]">
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">
Name (localStorage)
</span>
<div className="flex gap-2">
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Type something and refresh…"
className="h-8 text-sm"
/>
<Button size="sm" variant="outline" onClick={clearName}>
Clear
</Button>
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground w-24">
Count (persisted)
</span>
<div className="flex items-center gap-2">
<Button
size="icon"
variant="outline"
className="h-7 w-7 text-base"
onClick={() => setCount((c) => c - 1)}
>
−
</Button>
<span className="w-10 text-center font-mono text-sm px-2 py-0.5 rounded bg-muted border border-border">
{count}
</span>
<Button
size="icon"
variant="outline"
className="h-7 w-7 text-base"
onClick={() => setCount((c) => c + 1)}
>
+
</Button>
<Button size="sm" variant="ghost" onClick={clearCount}>
Reset
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground">
Values survive page refreshes via localStorage.
</p>
</div>
);
}Installation
Install the following dependencies:
Copy and paste the following code into your project:
import * as React from "react";
interface UsePersistedStateOptions<T> {
storage?: "local" | "session";
serialize?: (value: T) => string;
deserialize?: (raw: string) => T;
}
function getStorage(type: "local" | "session"): Storage | null {
if (typeof window === "undefined") return null;
return type === "session" ? window.sessionStorage : window.localStorage;
}
function usePersistedState<T>(
key: string,
defaultValue: T,
options?: UsePersistedStateOptions<T>,
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
const storageType = options?.storage ?? "local";
const serialize = options?.serialize ?? JSON.stringify;
const deserialize =
options?.deserialize ?? ((raw: string): T => JSON.parse(raw) as T);
const readFromStorage = React.useCallback((): T => {
const store = getStorage(storageType);
if (!store) return defaultValue;
try {
const raw = store.getItem(key);
if (raw === null) return defaultValue;
return deserialize(raw);
} catch {
return defaultValue;
}
}, [key, storageType, defaultValue, deserialize]);
const [value, setValueState] = React.useState<T>(() => readFromStorage());
const setValue = React.useCallback(
(next: T | ((prev: T) => T)) => {
setValueState((prev) => {
const resolved =
typeof next === "function" ? (next as (prev: T) => T)(prev) : next;
try {
const store = getStorage(storageType);
store?.setItem(key, serialize(resolved));
} catch {
// Silently ignore storage errors (e.g., quota exceeded)
}
return resolved;
});
},
[key, storageType, serialize],
);
const clear = React.useCallback(() => {
try {
const store = getStorage(storageType);
store?.removeItem(key);
} catch {
// Silently ignore
}
setValueState(defaultValue);
}, [key, storageType, defaultValue]);
// Sync across tabs (localStorage only)
React.useEffect(() => {
if (storageType !== "local") return;
function handleStorage(e: StorageEvent) {
if (e.key !== key) return;
if (e.newValue === null) {
setValueState(defaultValue);
} else {
try {
setValueState(deserialize(e.newValue));
} catch {
setValueState(defaultValue);
}
}
}
window.addEventListener("storage", handleStorage);
return () => window.removeEventListener("storage", handleStorage);
}, [key, storageType, defaultValue, deserialize]);
return [value, setValue, clear];
}
export default usePersistedState;Update the import paths to match your project setup.
Usage
const [theme, setTheme, clear] = usePersistedState("theme", "dark");
// With options
const [value, setValue, clear] = usePersistedState("my-key", defaultValue, {
storage: "session",
});Props
| Prop | Type | Default |
|---|---|---|
deserialize? | (value: string) => T | - |
serialize? | (value: T) => string | - |
storage? | "local" | "session" | - |
defaultValue | T | - |
key | string | - |