@technicalshree/use-localstorage
Version:
Feature-rich React hook that keeps state synchronised with localStorage across tabs and sessions.
241 lines (184 loc) • 8.17 kB
Markdown
# 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
```bash
npm install @technicalshree/use-localstorage
```
## Quick Start
```tsx
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 `storage` event.
- Accepts custom `serializer`/`deserializer` pairs for encryption, schema validation, or richer data types.
- Supports TTL eviction with `onExpire` callbacks 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 `useSyncedStorage` to target `localStorage`, `sessionStorage`, or custom adapters (including server-safe memory storage).
- Includes `useObjectLocalStorage` helpers for ergonomic partial updates and resets of nested state.
- Safe to import in SSR/SSG environments—the hook checks for the presence of `window` before touching storage.
## API
```ts
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)`: Persists `next` to state and `localStorage`. 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 `localStorage` when `window` exists, 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`? Use `useSyncedStorage` for a storage-agnostic API or `useObjectLocalStorage` for ergonomic nested updates (see below).
## Examples
### Persisting complex objects
```tsx
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
```tsx
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
```tsx
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
```tsx
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`
```tsx
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`
```tsx
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`](./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.md
```
## License
MIT © Krushna Raut