@technicalshree/use-localstorage
v0.2.2
Published
Feature-rich React hook that keeps state synchronised with localStorage across tabs and sessions.
Maintainers
Readme
useLocalStorage
A tiny React hook that keeps component state in sync with localStorage, so data persists across reloads and even across browser tabs. The API mirrors useState, making it a drop-in replacement when persistence is required.
When to Use
- Persist user preferences such as themes, locale, or layout density between visits.
- Maintain form drafts or onboarding progress without building a backend service.
- Cache lightweight API responses for faster repeat renders in the same browser session.
- Mirror authentication metadata that should survive refreshes but can remain client-side.
Installation
npm install @technicalshree/use-localstorageQuick Start
import { useLocalStorage } from '@technicalshree/use-localstorage';
function Preferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(current => (current === 'light' ? 'dark' : 'light'))}>
Theme: {theme}
</button>
);
}Features
- Reads the current value from
localStorage, falling back to the provided initial value. - Persists updates and synchronises across tabs using the browser
storageevent. - Accepts custom
serializer/deserializerpairs for encryption, schema validation, or richer data types. - Supports TTL eviction with
onExpirecallbacks and cross-tab expiry broadcasts. - Handles versioned payloads and migrations so older data can be upgraded on read.
- Normalises legacy string booleans (e.g.
'true','false') back into real booleans when the initial value is boolean. - Offers
useSyncedStorageto targetlocalStorage,sessionStorage, or custom adapters (including server-safe memory storage). - Includes
useObjectLocalStoragehelpers for ergonomic partial updates and resets of nested state. - Safe to import in SSR/SSG environments—the hook checks for the presence of
windowbefore touching storage.
API
const [value, setValue] = useLocalStorage<T>(
key: string,
initialValue: T | (() => T),
options?: UseLocalStorageOptions<T>
);| Parameter | Type | Description |
| --- | --- | --- |
| key | string | Storage key used in localStorage. |
| initialValue | T \| (() => T) | Value to use when nothing is stored. Lazy functions are invoked only when needed. |
| options | UseLocalStorageOptions<T> | Optional behaviour overrides detailed below. |
| Returns | Type | Description |
| --- | --- | --- |
| value | T | Current value pulled from storage or the initial fallback. |
| setValue | (next: T \| ((previous: T) => T) \| undefined) => void | Persists the next value. Passing undefined removes the key before falling back to the initial value. |
The hook returns a tuple identical to useState:
value: The current value (from storage or the initial value).setValue(next): Persistsnextto state andlocalStorage. Accepts either a value or an updater function.
Calling setValue(undefined) removes the key from storage and resets the hook back to the initial value.
Next.js & SSR: The hook only touches
localStoragewhenwindowexists, so it is safe to import in server-rendered bundles. Use it inside client components to avoid hydration warnings.
Options
| Option | Type | Description |
| --- | --- | --- |
| serializer | (value: T) => string | Transforms values before writing. Pair with deserializer for encryption or schema-aware persistence. Defaults to JSON.stringify. |
| deserializer | (raw: string) => T | Re-hydrates values read from storage. Defaults to JSON.parse. |
| ttl | number | Milliseconds until the entry expires. Expiry removes the key, resets state to the initial value, and emits callbacks/events. |
| onExpire | ({ key }: { key: string }) => void | Invoked when a value expires due to TTL. |
| onError | (error, context) => void | Notifies about serialization, deserialization, or storage errors. |
| onExternalChange | ({ key, value, event }) => void | Fired when another tab (or an expiry) updates the stored value. The value is undefined if the entry was removed. |
| version | number | Version tag stored alongside the value. Use with migrate to upgrade older payloads. |
| migrate | (value: T, storedVersion?: number) => T | Upgrades persisted data when the stored version differs from the current one. |
Need more than
localStorage? UseuseSyncedStoragefor a storage-agnostic API oruseObjectLocalStoragefor ergonomic nested updates (see below).
Examples
Persisting complex objects
type Profile = { id: string; darkMode: boolean };
const defaultProfile: Profile = { id: 'guest', darkMode: false };
export function ProfileSettings() {
const [profile, setProfile] = useLocalStorage<Profile>('profile', defaultProfile);
return (
<label>
<input
type="checkbox"
checked={profile.darkMode}
onChange={event =>
setProfile(previous => ({ ...previous, darkMode: event.target.checked }))
}
/>
Enable dark mode
</label>
);
}Boolean feature flags
export function ApprovalToggle() {
const [isApproved, setIsApproved] = useLocalStorage('is_approved', true);
return (
<button onClick={() => setIsApproved(previous => !previous)}>
{isApproved ? 'Approved' : 'Pending'}
</button>
);
}If older deployments stored the flag as a raw string (
"true"/"false"), the hook now converts those legacy values into real booleans on the first read and rewrites the stored value behind the scenes.
Reacting to cross-tab updates
export function ActiveSession() {
const [session, setSession] = useLocalStorage('session', { status: 'guest' });
useEffect(() => {
const keepAlive = setInterval(() => {
setSession(previous => ({ ...previous, refreshedAt: Date.now() }));
}, 60_000);
return () => clearInterval(keepAlive);
}, [setSession]);
return <span>Signed in as {session.status}</span>;
}
Time-to-live with expiry callbacks
function EphemeralNotice() {
const [dismissed, setDismissed] = useLocalStorage('notice', false, {
ttl: 60 * 60 * 1000,
onExpire: ({ key }) => console.info(`Entry ${key} expired`)
});
if (dismissed) {
return null;
}
return (
<button onClick={() => setDismissed(true)}>
Hide this notice for one hour
</button>
);
}Custom storage via useSyncedStorage
import { useSyncedStorage } from '@technicalshree/use-localstorage';
const sessionAdapter = {
getItem: (key: string) => sessionStorage.getItem(key),
setItem: (key: string, value: string) => sessionStorage.setItem(key, value),
removeItem: (key: string) => sessionStorage.removeItem(key)
};
export function SessionToken() {
const [token, setToken] = useSyncedStorage('session-token', null, {
storage: sessionAdapter,
ttl: 15 * 60 * 1000
});
return (
<button onClick={() => setToken(Math.random().toString(36).slice(2))}>
Rotate session token (expires in 15 minutes)
</button>
);
}Partial updates with useObjectLocalStorage
import { useObjectLocalStorage } from '@technicalshree/use-localstorage';
export function PreferencesPanel() {
const [preferences, , helpers] = useObjectLocalStorage('preferences', {
theme: 'light',
density: 'comfortable'
});
return (
<div>
<button onClick={() => helpers.setPartial({ theme: 'dark' })}>
Enable dark theme
</button>
<button onClick={() => helpers.reset()}>Reset defaults</button>
<pre>{JSON.stringify(preferences, null, 2)}</pre>
</div>
);
}Development
See DEVELOPMENT.md for detailed setup, testing, and publishing instructions.
Project Structure
use-localstorage/
├── src/index.ts # hook implementation and exports
├── tests/index.test.ts # Vitest coverage for hook behaviour
├── tsconfig.json # TypeScript configuration
├── tsup.config.ts # Bundler configuration
└── README.mdLicense
MIT © Krushna Raut
