react-persist-state-hook
v1.0.2
Published
A drop-in replacement for React.useState that persists state to localStorage with expiry, cross-tab sync, and SSR (Next.js) support.
Maintainers
Readme
react-persist-state-hook
A drop-in replacement for React.useState that automatically persists state to localStorage — with built-in support for expiry, cross-tab sync, and full SSR / Next.js compatibility.
No extra setup. No context providers. No wrappers. Just swap useState with usePersistentState and your state survives page refreshes.
Features
- localStorage sync — state is automatically saved and restored across page reloads
- Expiry — optionally auto-clear state after a set number of minutes
- Cross-tab sync — state updates in real time across all open browser tabs
- SSR safe — works in Next.js and any server-side rendering environment without throwing
- TypeScript first — full generic type support out of the box
- Zero dependencies — only React as a peer dependency
- Tiny — under 2kb minified and gzipped
Installation
npm install react-persist-state-hookyarn add react-persist-state-hookpnpm add react-persist-state-hookQuick Start
import { usePersistentState } from "react-persist-state-hook";
function ThemeToggle() {
const [theme, setTheme] = usePersistentState("theme", "light");
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Current theme: {theme}
</button>
);
}That is all you need. The theme will persist across page refreshes automatically.
API
const [state, setState, clear] = usePersistentState(key, defaultValue, options);Parameters
| Parameter | Type | Required | Description |
| -------------- | ---------------- | -------- | ----------------------------------------- |
| key | string | Yes | Unique localStorage key |
| defaultValue | T | Yes | Initial value used when nothing is stored |
| options | PersistOptions | No | Configuration — see below |
Returns
| Value | Type | Description |
| ---------- | ----------------------------- | ------------------------------------------------------------- |
| state | T | Current state value |
| setState | Dispatch<SetStateAction<T>> | Same as React.useState setter — supports functional updates |
| clear | () => void | Removes the value from localStorage and resets to default |
Options
| Option | Type | Default | Description |
| ---------------- | ---------------------- | ---------------- | ------------------------------------------- |
| expiryMinutes | number | undefined | Auto-clear state after this many minutes |
| syncAcrossTabs | boolean | true | Sync state changes across open browser tabs |
| serializer | (value: T) => string | JSON.stringify | Custom serializer |
| deserializer | (value: string) => T | JSON.parse | Custom deserializer |
Examples
Complete single-app demo (all features in one file)
If you want one GitHub-ready example that shows everything together, use:
It demonstrates:
- basic string persistence
- typed object persistence using generics
- expiry (
expiryMinutes) with live value updates - cross-tab sync ON and OFF (
syncAcrossTabs) - custom
serializeranddeserializerwithDate - functional updater (
setState(prev => ...)) - manual clear via the
clearfunction
Minimal usage inside your app:
import FullFeaturesDemo from "./examples/FullFeaturesDemo";
export default function App() {
return <FullFeaturesDemo />;
}If your example app is in a different repository, copy FullFeaturesDemo.tsx
and change the import line to:
import { usePersistentState, type PersistOptions } from "react-persist-state-hook";Basic usage — persists across page reloads
const [username, setUsername] = usePersistentState("username", "");With expiry — auto-clears after 30 minutes
const [token, setToken, clearToken] = usePersistentState("auth-token", null, {
expiryMinutes: 30,
});
// Log user out manually
function logout() {
clearToken();
}With cross-tab sync disabled
const [count, setCount] = usePersistentState("count", 0, {
syncAcrossTabs: false,
});Functional updater — works exactly like useState
const [count, setCount] = usePersistentState("count", 0);
// Both of these work
setCount(5);
setCount((prev) => prev + 1);With objects and arrays
interface UserPreferences {
theme: "light" | "dark";
language: string;
notifications: boolean;
}
const [prefs, setPrefs, clearPrefs] = usePersistentState<UserPreferences>(
"user-prefs",
{ theme: "light", language: "en", notifications: true },
);
// Update a single field
setPrefs((prev) => ({ ...prev, theme: "dark" }));With custom serializer (e.g. for Date objects)
const [lastVisit, setLastVisit] = usePersistentState<Date>(
"last-visit",
new Date(),
{
serializer: (date) => date.toISOString(),
deserializer: (str) => new Date(str),
},
);SSR / Next.js — works without any extra setup
// This works in a Next.js page or app router component
// No "use client" guards needed around the hook itself
const [theme, setTheme] = usePersistentState("theme", "light");The hook detects the server environment and safely returns the default value during SSR, then hydrates from localStorage on the client.
How cross-tab sync works
When syncAcrossTabs is enabled (the default), the hook listens to the browser's native storage event. This event fires automatically in all other tabs when localStorage is updated — so any state change in one tab is instantly reflected in every other open tab without any polling or websockets.
Tab A: setTheme("dark") → Tab B and Tab C automatically update to "dark"TypeScript
The hook is fully generic and infers the type from your default value automatically.
// Type is inferred as string
const [name, setName] = usePersistentState("name", "");
// Type is inferred as number
const [count, setCount] = usePersistentState("count", 0);
// Explicit generic for complex types
const [user, setUser] = usePersistentState<User | null>("user", null);PersistOptions type
type PersistOptions<T> = {
expiryMinutes?: number;
syncAcrossTabs?: boolean; // default: true
serializer?: (value: T) => string;
deserializer?: (value: string) => T;
};Passing options with explicit type
type Session = { token: string; issuedAt: number };
const options: PersistOptions<Session | null> = {
expiryMinutes: 30,
syncAcrossTabs: true,
serializer: (value) => JSON.stringify(value),
deserializer: (value) => JSON.parse(value) as Session | null,
};
const [session, setSession, clearSession] = usePersistentState<Session | null>(
"session",
null,
options,
);Browser Support
Works in all modern browsers that support localStorage and the storage event.
- Chrome 4+
- Firefox 3.5+
- Safari 4+
- Edge 12+
Running Tests
npm install
npm testFor coverage report:
npm run test:coverageContributing
Pull requests are welcome. For major changes please open an issue first to discuss what you would like to change.
- Fork the repo
- Create your feature branch (
git checkout -b feature/your-feature) - Commit your changes (
git commit -m "add your feature") - Push to the branch (
git push origin feature/your-feature) - Open a pull request
License
MIT — see LICENSE for details.
Author
Debabrata Basak linkedin.com/in/coderboy061 | github.com/coderboy061/react-persist-state-hook
