safe-local-store
v1.0.0
Published
localStorage with automated schema versioning, defaults, and sequential migrations.
Maintainers
Readme
safe-local-store
A type-safe localStorage wrapper with automated schema versioning, sequential migrations, fail-safe fallback boundaries, and reactively synced multi-tab updates.
🚀 Features
- 🔄 Automated & Sequential Schema Migrations: Automatically detects outdated data and migrates it step-by-step (e.g., $1 \rightarrow 2 \rightarrow 3$) to the latest version.
- ⚡ Reactive Subscriptions: Subscribe to changes in state. Automatically listens for updates in other tabs/windows via browser
storageevents to keep your UI in sync. - 🛠️ SSR & Prerendering Safe: Automatically falls back to in-memory store in server-side environments (Node.js, Next.js, Remix, Nuxt), preventing reference errors and build-time crashes.
- 🛡️ Fail-Safe Defaults & Error Handling: Gracefully recovers and resets to default values if JSON parsing fails or migrations crash.
- 📦 Fully Typed: Built with TypeScript for precise autocompletion and type inference.
- 🍃 Zero Dependencies & Tiny Size: Lightweight build bundle supporting both ES Modules (ESM) and CommonJS (CJS).
📦 Installation
npm install safe-local-storeor with yarn / pnpm:
yarn add safe-local-store
pnpm add safe-local-store🛠️ Usage
Basic Example
import { createStore } from 'safe-local-store';
interface Project {
id: string;
name: string;
isArchived?: boolean; // Added in version 2
}
interface ProjectState {
list: Project[];
selectedId: string | null;
}
const projectStore = createStore<ProjectState>({
key: 'user-projects',
version: 2, // Current schema version
default: () => ({ list: [], selectedId: null }),
// Migrations run automatically if stored version < current version
migrations: {
2: (oldData: any) => ({
...oldData,
list: oldData.list.map((p: any) => ({ ...p, isArchived: false }))
})
}
});
// Retrieve always valid and migrated data
const data = projectStore.get();
console.log(data.list);
// Set new state (supports static value or functional updates)
projectStore.set({
list: [{ id: '1', name: 'My First Project', isArchived: false }],
selectedId: '1'
});
// Or using functional update:
projectStore.set(prev => ({
...prev,
selectedId: null
}));React Hooks Integration Example
You can easily bind the store to your UI framework. Here is a simple React hook integration using useSyncExternalStore:
import { useSyncExternalStore } from 'react';
import { createStore } from 'safe-local-store';
const settingsStore = createStore({
key: 'app-settings',
version: 1,
default: () => ({ theme: 'light' })
});
export function useAppSettings() {
const state = useSyncExternalStore(
settingsStore.subscribe,
settingsStore.get, // Client getter
settingsStore.get // Server/SSR fallback (returns default theme)
);
return [state, settingsStore.set] as const;
}Vue Composable Integration Example
You can also bind the store to Vue 3 reactive states using a custom composable:
import { ref, onUnmounted, readonly } from 'vue';
import { createStore } from 'safe-local-store';
const settingsStore = createStore({
key: 'app-settings',
version: 1,
default: () => ({ theme: 'light' })
});
export function useAppSettings() {
const state = ref(settingsStore.get());
// Subscribe to changes (including cross-tab updates)
const unsubscribe = settingsStore.subscribe((newValue) => {
state.value = newValue;
});
// Clean up subscription on unmount
onUnmounted(() => {
unsubscribe();
});
return {
state: readonly(state),
setSettings: settingsStore.set
};
}⚙️ Configuration Options (StoreOptions<T>)
| Property | Type | Required | Description |
| :--- | :--- | :---: | :--- |
| key | string | Yes | The localStorage key under which the data is saved. |
| version | number | Yes | Integer version of the schema ($\ge 1$). |
| default | () => T | Yes | Factory function returning the default state. |
| migrations | Record<number, (old: any) => any> | No | Registry of version migration actions. |
| storage | StorageLike | No | Custom storage provider (defaults to localStorage). |
| serialize | (value: any) => string | No | Encoder (defaults to JSON.stringify). |
| deserialize | (str: string) => any | No | Decoder (defaults to JSON.parse). |
| onError | (error: any) => void | No | Error handler fallback (defaults to console.error). |
🔧 Store API
A store instance returned by createStore<T> exposes:
get(): T: Returns the current validated/migrated state object.set(value: T \| ((prev: T) => T)): void: Saves new value to storage and triggers subscribers.clear(): void: Resets the store back to its default value.subscribe(listener: (value: T) => void): () => void: Registers a callback for state changes. Returns an unsubscribe function.
📄 License
MIT © 2026
