Skip to main content
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.
table
string
required
Storage key name
init
any
required
Initial value if key doesn’t exist

set()

set(table: string, changes: Change[]): void
Saves changes to storage.
table
string
required
Storage key name
changes
Change[]
required
Array of changes to apply

getMetadata()

getMetadata(table: string): PersistMetadata
Retrieves sync metadata (lastSync, pending changes).
table
string
required
Storage key name

setMetadata()

setMetadata(table: string, metadata: PersistMetadata): void
Saves sync metadata.
table
string
required
Storage key name
metadata
PersistMetadata
required
Metadata to save

deleteTable()

deleteTable(table: string): void
Removes data from storage.
table
string
required
Storage key name

deleteMetadata()

deleteMetadata(table: string): void
Removes metadata from storage.
table
string
required
Storage key name

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

With Transform

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

Storage Format

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

  1. Use meaningful names: Choose unique, descriptive names for each persisted observable
  2. Watch storage limits: localStorage is limited to ~5MB per origin
  3. Use sessionStorage for temporary data: Shopping carts, form drafts, etc.
  4. Handle quota errors: Catch QuotaExceededError for large data
  5. 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

Build docs developers (and LLMs) love