UNPKG

@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
# 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