The localStorage plugin persists observable data to the browser’s localStorage or sessionStorage. This is the simplest persistence option for web applications.
Installation
npm install @legendapp/state
Usage
import { synced } from '@legendapp/state/sync'
import { ObservablePersistLocalStorage } from '@legendapp/state/persist-plugins/local-storage'
const user$ = synced({
get: () => fetch('/api/user').then(r => r.json()),
persist: {
name: 'user',
plugin: ObservablePersistLocalStorage
}
})
Plugins
ObservablePersistLocalStorage
Persists to localStorage (data survives browser restarts).
import { ObservablePersistLocalStorage } from '@legendapp/state/persist-plugins/local-storage'
const data$ = synced({
persist: {
name: 'myData',
plugin: ObservablePersistLocalStorage
}
})
ObservablePersistSessionStorage
Persists to sessionStorage (data cleared when tab closes).
import { ObservablePersistSessionStorage } from '@legendapp/state/persist-plugins/local-storage'
const tempData$ = synced({
persist: {
name: 'tempData',
plugin: ObservablePersistSessionStorage
}
})
Global Configuration
import { configureObservableSync } from '@legendapp/state/sync'
import { ObservablePersistLocalStorage } from '@legendapp/state/persist-plugins/local-storage'
configureObservableSync({
persist: {
plugin: ObservablePersistLocalStorage
}
})
// Now all synced observables use localStorage by default
const data$ = synced({
persist: { name: 'data' } // Uses localStorage
})
Plugin API
The localStorage plugin implements the ObservablePersistPlugin interface:
getTable()
getTable(table: string, init: any): any
Retrieves data from storage.
Initial value if key doesn’t exist
set()
set(table: string, changes: Change[]): void
Saves changes to storage.
Array of changes to apply
getMetadata(table: string): PersistMetadata
Retrieves sync metadata (lastSync, pending changes).
setMetadata(table: string, metadata: PersistMetadata): void
Saves sync metadata.
deleteTable()
deleteTable(table: string): void
Removes data from storage.
deleteMetadata(table: string): void
Removes metadata from storage.
Examples
Basic Usage
import { synced } from '@legendapp/state/sync'
import { ObservablePersistLocalStorage } from '@legendapp/state/persist-plugins/local-storage'
const settings$ = synced({
initial: {
theme: 'light',
notifications: true
},
persist: {
name: 'app-settings',
plugin: ObservablePersistLocalStorage
}
})
// Data is automatically persisted on changes
settings$.theme.set('dark')
// Data is automatically loaded on page refresh
console.log(settings$.theme.get()) // 'dark'
With Remote Sync
const user$ = synced({
get: () => fetch('/api/user').then(r => r.json()),
set: ({ value }) => fetch('/api/user', {
method: 'PUT',
body: JSON.stringify(value)
}),
persist: {
name: 'user',
plugin: ObservablePersistLocalStorage
}
})
// 1. Loads from localStorage immediately (fast)
// 2. Syncs with server in background
// 3. Saves to both localStorage and server on changes
Session Storage for Temporary Data
import { ObservablePersistSessionStorage } from '@legendapp/state/persist-plugins/local-storage'
// Shopping cart cleared when tab closes
const cart$ = synced({
initial: { items: [] },
persist: {
name: 'shopping-cart',
plugin: ObservablePersistSessionStorage
}
})
Multiple Observables
const user$ = synced({
persist: {
name: 'user', // Stored at key 'user'
plugin: ObservablePersistLocalStorage
}
})
const settings$ = synced({
persist: {
name: 'settings', // Stored at key 'settings'
plugin: ObservablePersistLocalStorage
}
})
// Stored separately in localStorage:
// localStorage.getItem('user') -> user data
// localStorage.getItem('settings') -> settings data
const data$ = synced({
persist: {
name: 'data',
plugin: ObservablePersistLocalStorage,
transform: {
// Dates are converted to ISO strings for storage
save: (value) => ({
...value,
createdAt: value.createdAt.toISOString()
}),
// ISO strings converted back to Dates when loaded
load: (value) => ({
...value,
createdAt: new Date(value.createdAt)
})
}
}
})
Clear Persisted Data
import { syncState } from '@legendapp/state/sync'
const data$ = synced({
persist: {
name: 'data',
plugin: ObservablePersistLocalStorage
}
})
// Clear persisted data
const state = syncState(data$)
await state.resetPersistence()
// localStorage.getItem('data') -> null
Data is stored as JSON strings:
const user$ = synced({
initial: { name: 'John', age: 30 },
persist: {
name: 'user',
plugin: ObservablePersistLocalStorage
}
})
// localStorage contents:
// key: 'user'
// value: '{"name":"John","age":30}'
// Metadata stored separately:
// key: 'user__m'
// value: '{"lastSync":1699564800000,"pending":{}}'
Storage Limits
localStorage has a storage limit (typically 5-10MB per origin):
try {
const largeData$ = synced({
persist: {
name: 'largeData',
plugin: ObservablePersistLocalStorage
}
})
// May throw QuotaExceededError
largeData$.set(veryLargeObject)
} catch (error) {
if (error.name === 'QuotaExceededError') {
console.error('localStorage quota exceeded')
// Consider using IndexedDB for large data
}
}
Server-Side Rendering
The plugin gracefully handles SSR environments where localStorage is undefined:
// Safe to use in SSR
const data$ = synced({
persist: {
name: 'data',
plugin: ObservablePersistLocalStorage
}
})
// On server: no-op (doesn't crash)
// On client: uses localStorage
Testing
In test environments, the plugin uses a mock storage:
// In Jest/Vitest tests
import { ObservablePersistLocalStorage } from '@legendapp/state/persist-plugins/local-storage'
test('persists data', () => {
const data$ = synced({
initial: { count: 0 },
persist: {
name: 'test-data',
plugin: ObservablePersistLocalStorage
}
})
data$.count.set(5)
// Uses globalThis._testlocalStorage in tests
})
Best Practices
- Use meaningful names: Choose unique, descriptive names for each persisted observable
- Watch storage limits: localStorage is limited to ~5MB per origin
- Use sessionStorage for temporary data: Shopping carts, form drafts, etc.
- Handle quota errors: Catch
QuotaExceededError for large data
- Consider IndexedDB: For large datasets or complex queries
When to Use
Use localStorage when:
- Data is small (< 1MB)
- Simple key-value storage is sufficient
- You need synchronous access
- Supporting older browsers
Use IndexedDB when:
- Data is large (> 1MB)
- Need to store binary data
- Need complex queries
- Want better performance for large datasets
See Also