@lucasriondel/use-local-storage-reacthook
v0.0.1
Published
A React hook for managing local storage
Maintainers
Readme
💾 useLocalStorageState
A powerful React hook for managing state that's automatically persisted to localStorage. Perfect for maintaining user preferences, form data, and application settings across browser sessions with automatic synchronization and data migration support.
✨ Features
- 🎯 Type-safe - Full TypeScript support with generic types
- 💾 Automatic localStorage sync - State changes are persisted instantly
- 🔄 Cross-tab synchronization - Share state changes across browser tabs
- 🛡️ Validation - Built-in sanitization and validation hooks
- 📦 Custom serialization - Define custom codecs for complex data types
- 🔄 Data migration - Handle schema changes with version migration
- ⚡ Performance optimized - Efficient serialization and change detection
- 🪶 Lightweight - Zero dependencies (except React peer dependency)
📦 Installation
# Using npm
npm install @lucasriondel/use-local-storage-reacthook
# Using yarn
yarn add @lucasriondel/use-local-storage-reacthook
# Using pnpm
pnpm add @lucasriondel/use-local-storage-reacthook🚀 Quick Start
import { useLocalStorageState } from "@lucasriondel/use-local-storage-reacthook";
function UserPreferences() {
const [settings, settingsApi] = useLocalStorageState(
{
theme: "light",
language: "en",
notifications: true,
},
{ key: "user-preferences" }
);
return (
<div>
<select
value={settings.theme}
onChange={(e) => settingsApi.set("theme", e.target.value)}
>
<option value="light">Light Theme</option>
<option value="dark">Dark Theme</option>
</select>
<select
value={settings.language}
onChange={(e) => settingsApi.set("language", e.target.value)}
>
<option value="en">English</option>
<option value="fr">French</option>
<option value="es">Spanish</option>
</select>
<label>
<input
type="checkbox"
checked={settings.notifications}
onChange={(e) => settingsApi.set("notifications", e.target.checked)}
/>
Enable Notifications
</label>
</div>
);
}Your settings will automatically persist across browser sessions!
📚 API Reference
useLocalStorageState(defaults, options)
Returns a tuple [state, api] where:
state: The current state objectapi: Object with methods to manipulate the state
Parameters
| Parameter | Type | Description |
| ---------- | ------------------------------------------ | ---------------------------- |
| defaults | DeepPartial<T> \| (() => DeepPartial<T>) | Default values for the state |
| options | LocalStorageStateOptions<T> | Configuration options |
Options
| Option | Type | Default | Description |
| ---------------- | -------------------------------------------------- | ----------- | ------------------------------------------------- |
| key | string | Required | The localStorage key to store data under |
| codecs | Partial<{ [K in keyof T]: Codec<T[K]> }> | {} | Custom serialization for specific properties |
| sanitize | (draft: DeepPartial<T>) => DeepPartial<T> | undefined | Validation/sanitization function |
| onChange | (state: T, meta) => void | undefined | Callback fired on state changes |
| syncAcrossTabs | boolean | true | Sync state changes across browser tabs |
| version | number | undefined | Version number for data migration |
| migrate | (stored: unknown, version: number) => Partial<T> | undefined | Function to migrate old data when version changes |
API Methods
| Method | Signature | Description |
| ---------- | ---------------------------------------------------- | ---------------------------------------------- |
| setState | (updater: T \| (prev: T) => T) => void | Replace entire state |
| get | (key: keyof T) => T[key] \| undefined | Get value of specific property |
| set | (key: keyof T, value: T[key] \| undefined) => void | Set specific property (or delete if undefined) |
| patch | (partial: DeepPartial<T>) => void | Merge partial changes |
| remove | (...keys: (keyof T)[]) => void | Remove one or more properties |
| clear | () => void | Clear all data from localStorage |
🎯 Examples
Basic Usage
import { useLocalStorageState } from "use-local-storage-reacthook";
function App() {
const [profile, profileApi] = useLocalStorageState(
{ name: "", age: 0 },
{ key: "user-profile" }
);
return (
<div>
<input
value={profile.name}
onChange={(e) => profileApi.set("name", e.target.value)}
placeholder="Enter your name"
/>
<input
type="number"
value={profile.age}
onChange={(e) => profileApi.set("age", parseInt(e.target.value) || 0)}
placeholder="Enter your age"
/>
<button onClick={() => profileApi.clear()}>Clear Profile</button>
</div>
);
}With Custom Serialization
interface AppSettings {
tags: string[];
lastLoginDate: Date;
preferences: { theme: string; lang: string };
}
const [settings, settingsApi] = useLocalStorageState<AppSettings>(
{
tags: [],
lastLoginDate: new Date(),
preferences: { theme: "light", lang: "en" },
},
{
key: "app-settings",
codecs: {
tags: {
parse: (str) => str.split(",").filter(Boolean),
format: (tags) => tags.join(","),
},
lastLoginDate: {
parse: (str) => new Date(str),
format: (date) => date.toISOString(),
},
},
}
);With Validation and Change Tracking
const [userPrefs, prefsApi] = useLocalStorageState(
{
theme: "light",
fontSize: 16,
language: "en",
},
{
key: "user-preferences",
sanitize: (draft) => ({
theme: ["light", "dark"].includes(draft.theme) ? draft.theme : "light",
fontSize: Math.max(12, Math.min(24, draft.fontSize || 16)),
language: ["en", "fr", "es"].includes(draft.language)
? draft.language
: "en",
}),
onChange: (newState, { source }) => {
console.log(`Preferences updated from ${source}:`, newState);
// Send analytics, trigger theme updates, etc.
},
}
);Multiple Hook Instances with Different Keys
function Dashboard() {
// User filters stored under 'user-filters' key
const [userFilters, userApi] = useLocalStorageState(
{
role: "all",
department: "all",
},
{ key: "user-filters" }
);
// Product filters stored under 'product-filters' key
const [productFilters, productApi] = useLocalStorageState(
{
category: "all",
inStock: true,
},
{ key: "product-filters" }
);
// Each hook manages its own localStorage entry independently
// localStorage: { "user-filters": {...}, "product-filters": {...} }
}Complex State Management with Data Migration
interface AppState {
filters: {
search: string;
category: string[];
priceRange: [number, number];
};
view: "grid" | "list";
sort: { field: string; direction: "asc" | "desc" };
}
const [appState, api] = useLocalStorageState<AppState>(
{
filters: {
search: "",
category: [],
priceRange: [0, 1000],
},
view: "grid",
sort: { field: "name", direction: "asc" },
},
{
key: "app-state",
version: 2,
migrate: (stored, version) => {
if (version < 2) {
// Migrate from v1: add new priceRange field
const oldState = stored as any;
return {
...oldState,
filters: {
...oldState.filters,
priceRange: [0, 1000], // Add default price range
},
};
}
return stored as Partial<AppState>;
},
}
);
// Update nested properties
api.patch({
filters: {
...appState.filters,
search: "new search term",
},
});
// Toggle sort direction
api.set("sort", {
...appState.sort,
direction: appState.sort.direction === "asc" ? "desc" : "asc",
});🔧 Advanced Configuration
Cross-Tab Synchronization
// Enable cross-tab sync (default)
const [state, api] = useLocalStorageState(defaults, {
key: "shared-state",
syncAcrossTabs: true,
});
// Disable cross-tab sync for performance or privacy
const [state, api] = useLocalStorageState(defaults, {
key: "local-only-state",
syncAcrossTabs: false,
});Data Migration Between Versions
const [config, configApi] = useLocalStorageState(
{ apiUrl: "https://api.example.com", timeout: 5000 },
{
key: "app-config",
version: 3,
migrate: (stored, currentVersion) => {
const data = stored as any;
if (currentVersion < 2) {
// v1 -> v2: rename 'endpoint' to 'apiUrl'
data.apiUrl = data.endpoint;
delete data.endpoint;
}
if (currentVersion < 3) {
// v2 -> v3: add timeout field
data.timeout = data.timeout || 5000;
}
return data;
},
}
);Error Handling and Validation
const [userInput, inputApi] = useLocalStorageState(
{ email: "", age: 0 },
{
key: "user-input",
sanitize: (draft) => {
// Validate and sanitize data from localStorage
const email = typeof draft.email === "string" ? draft.email : "";
const age =
typeof draft.age === "number" && draft.age >= 0 ? draft.age : 0;
return { email, age };
},
onChange: (newState, { source }) => {
if (source === "external") {
console.log("State updated from another tab:", newState);
}
},
}
);📝 TypeScript Support
The hook is fully typed and provides excellent TypeScript integration:
interface UserProfile {
name: string;
roles: ("admin" | "user" | "guest")[];
isActive: boolean;
metadata?: { lastLogin: Date };
}
// Full type safety
const [profile, profileApi] = useLocalStorageState<UserProfile>(
{
name: "",
roles: [],
isActive: true,
},
{ key: "user-profile" }
);
// TypeScript knows the exact shape
profileApi.set("name", "john"); // ✅ Valid
profileApi.set("roles", ["admin", "user"]); // ✅ Valid
profileApi.set("invalidProp", "value"); // ❌ TypeScript error
// Partial updates are also type-safe
profileApi.patch({
name: "jane",
isActive: false,
}); // ✅ Valid
profileApi.patch({
invalidField: true,
}); // ❌ TypeScript error🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
🙏 Acknowledgments
- Inspired by the need for better localStorage state management in React applications
- Built with TypeScript for maximum developer experience
- Designed to handle complex state persistence scenarios with ease
- Supports modern React patterns and best practices
Happy coding! 🚀 If you find this hook useful, please consider giving it a ⭐ on GitHub!
