Documentation Index
Fetch the complete documentation index at: https://mintlify.com/reatom/reatom/llms.txt
Use this file to discover all available pages before exploring further.
State Persistence
Reatom provides a powerful persistence system that automatically synchronizes atom state with various storage backends, including localStorage, sessionStorage, IndexedDB, and custom storage implementations.
Core Concepts
Reatom’s persistence system offers:
- Automatic Synchronization - State syncs to storage on every change
- Cross-Tab Sync - Changes in one tab update other tabs automatically
- Schema Validation - Validate persisted data with Standard Schema
- Versioning & Migration - Handle schema changes gracefully
- TTL Support - Automatically expire old data
- Custom Storage - Implement any storage backend
Quick Start
LocalStorage Persistence
import { atom } from '@reatom/core'
import { withLocalStorage } from '@reatom/core/persist'
const themeAtom = atom('light', 'themeAtom').extend(
withLocalStorage('app-theme')
)
// State automatically persists to localStorage
themeAtom.set('dark')
// On page reload, state is restored
console.log(themeAtom()) // → 'dark'
SessionStorage Persistence
import { withSessionStorage } from '@reatom/core/persist'
const wizardStateAtom = atom({ step: 1 }, 'wizardStateAtom').extend(
withSessionStorage('wizard-progress')
)
// Persists for the session duration only
wizardStateAtom.set({ step: 2 })
// Cleared when tab closes
Storage Options
LocalStorage
Persists data between browser sessions:
import { withLocalStorage } from '@reatom/core/persist'
const userPrefsAtom = atom(
{ fontSize: 14, theme: 'light' },
'userPrefsAtom'
).extend(withLocalStorage('user-preferences'))
// Data survives:
// ✓ Page reloads
// ✓ Browser restarts
// ✓ System reboots
Features:
- ~5-10MB storage limit (varies by browser)
- Shared across all tabs
- Cross-tab synchronization via storage events
- Automatic fallback to memory storage if unavailable
SessionStorage
Persists data for the current session only:
import { withSessionStorage } from '@reatom/core/persist'
const formDraftAtom = atom(
{ title: '', content: '' },
'formDraftAtom'
).extend(withSessionStorage('form-draft'))
// Data survives:
// ✓ Page reloads
// ✗ Tab closes
// ✗ Browser restarts
Features:
- ~5-10MB storage limit (varies by browser)
- Isolated per tab (no cross-tab sharing)
- Cleared when tab closes
- Automatic fallback to memory storage if unavailable
IndexedDB
Store large amounts of structured data:
import { withIndexedDb } from '@reatom/core/persist/web-storage'
const documentsAtom = atom([], 'documentsAtom').extend(
withIndexedDb('my-documents')
)
// Store large datasets
documentsAtom.set([
{ id: 1, content: '...large content...' },
{ id: 2, content: '...large content...' },
// ... hundreds or thousands of items
])
Features:
- Much larger storage limits (often 50MB+)
- Async operations (non-blocking)
- Supports complex data structures
- Cross-tab synchronization
Cookies
Store small amounts of data that need server access:
import { withCookie } from '@reatom/core/persist/web-storage'
const authTokenAtom = atom('', 'authTokenAtom').extend(
withCookie('auth-token')
)
// Accessible in HTTP requests
authTokenAtom.set('bearer-token-xyz')
Features:
- ~4KB size limit per cookie
- Sent with HTTP requests
- Can set expiration, domain, path
- Useful for SSR applications
Advanced Configuration
Transform data before persisting:
import { withLocalStorage } from '@reatom/core/persist'
interface User {
id: number
name: string
createdAt: Date
}
const userAtom = atom<User | null>(null, 'userAtom').extend(
withLocalStorage({
key: 'current-user',
toSnapshot: (user) => {
if (!user) return null
return {
...user,
createdAt: user.createdAt.toISOString(),
}
},
fromSnapshot: (snapshot) => {
if (!snapshot) return null
return {
...snapshot,
createdAt: new Date(snapshot.createdAt),
}
},
})
)
Schema Validation
Validate persisted data with schemas:
import { z } from 'zod'
import { withLocalStorage } from '@reatom/core/persist'
const settingsSchema = z.object({
theme: z.enum(['light', 'dark']),
fontSize: z.number().min(10).max(24),
notifications: z.boolean(),
})
const settingsAtom = atom(
{ theme: 'light', fontSize: 14, notifications: true },
'settingsAtom'
).extend(
withLocalStorage({
key: 'app-settings',
schema: settingsSchema,
})
)
// Invalid data is rejected and atom uses default value
// localStorage.setItem('app-settings', JSON.stringify({ theme: 'blue' }))
// settingsAtom() // → { theme: 'light', fontSize: 14, notifications: true }
Versioning & Migration
Handle breaking changes with versions:
import { withLocalStorage } from '@reatom/core/persist'
// Version 1 schema
interface SettingsV1 {
darkMode: boolean
}
// Version 2 schema
interface SettingsV2 {
theme: 'light' | 'dark'
fontSize: number
}
const settingsAtom = atom<SettingsV2>(
{ theme: 'light', fontSize: 14 },
'settingsAtom'
).extend(
withLocalStorage({
key: 'settings',
version: 2,
migration: (record, currentVersion) => {
if (record.version === 1) {
const v1Data = record.data as SettingsV1
return {
theme: v1Data.darkMode ? 'dark' : 'light',
fontSize: 14,
}
}
return record.data
},
})
)
Time-To-Live (TTL)
Automatically expire data:
import { withLocalStorage } from '@reatom/core/persist'
const cacheAtom = atom(null, 'cacheAtom').extend(
withLocalStorage({
key: 'api-cache',
time: 1000 * 60 * 60, // 1 hour in milliseconds
})
)
// Data is cleared after 1 hour
cacheAtom.set({ data: '...' })
// After 1 hour, cacheAtom() returns initial value
Cross-Tab Synchronization
Automatic Sync
LocalStorage automatically syncs across tabs:
import { withLocalStorage } from '@reatom/core/persist'
const counterAtom = atom(0, 'counterAtom').extend(
withLocalStorage('shared-counter')
)
// Tab 1
counterAtom.set(5)
Automatically Updates Tab 2
// Tab 2 (automatically updated)
console.log(counterAtom()) // → 5
Disable Sync
Disable cross-tab synchronization:
import { reatomPersist } from '@reatom/core/persist'
import { createMemStorage } from '@reatom/core/persist'
const withLocalStorageNoSync = reatomPersist(
createMemStorage({
name: 'no-sync',
subscribe: false, // Disable cross-tab sync
})
)
const atomAtom = atom(0, 'atomAtom').extend(
withLocalStorageNoSync('key')
)
Custom Storage
Creating Custom Storage
import { reatomPersist, type PersistStorage } from '@reatom/core/persist'
const createCustomStorage = (name: string): PersistStorage => ({
name,
cache: new Map(),
get({ key }) {
const value = myCustomDb.get(key)
return value ? JSON.parse(value) : null
},
set({ key }, record) {
myCustomDb.set(key, JSON.stringify(record))
},
clear({ key }) {
myCustomDb.delete(key)
},
subscribe({ key }, callback) {
const listener = (event) => {
if (event.key === key) {
callback(JSON.parse(event.value))
}
}
myCustomDb.on('change', listener)
return () => myCustomDb.off('change', listener)
},
})
const withMyStorage = reatomPersist(createCustomStorage('myStorage'))
Async Storage
Implement async storage backends:
import { reatomPersist } from '@reatom/core/persist'
const withAsyncStorage = reatomPersist({
name: 'asyncStorage',
cache: new Map(),
async get({ key }) {
const value = await myAsyncDb.get(key)
return value ? JSON.parse(value) : null
},
async set({ key }, record) {
await myAsyncDb.set(key, JSON.stringify(record))
},
async clear({ key }) {
await myAsyncDb.delete(key)
},
})
const dataAtom = atom(null, 'dataAtom').extend(
withAsyncStorage('my-data')
)
Memory Storage
In-memory storage for testing:
import { createMemStorage, reatomPersist } from '@reatom/core/persist'
const withTestStorage = reatomPersist(
createMemStorage({
name: 'test',
snapshot: {
'user-id': 123,
'theme': 'dark',
},
})
)
// Use in tests
const userIdAtom = atom(0, 'userIdAtom').extend(
withTestStorage('user-id')
)
console.log(userIdAtom()) // → 123 (from snapshot)
Best Practices
1. Use Appropriate Storage
Choose the right storage for your use case:
// ✓ Good - settings persist across sessions
const settingsAtom = atom({}, 'settingsAtom').extend(
withLocalStorage('settings')
)
// ✓ Good - wizard state only for current session
const wizardAtom = atom({}, 'wizardAtom').extend(
withSessionStorage('wizard')
)
// ✗ Bad - auth token should use httpOnly cookies
const tokenAtom = atom('', 'tokenAtom').extend(
withLocalStorage('auth-token') // Security risk!
)
2. Always Validate Persisted Data
Never trust persisted data:
// ✓ Good - validated with schema
const settingsAtom = atom({}, 'settingsAtom').extend(
withLocalStorage({
key: 'settings',
schema: settingsSchema,
})
)
// ✗ Bad - no validation
const settingsAtom = atom({}, 'settingsAtom').extend(
withLocalStorage('settings')
)
3. Use Versioning for Production
Always version your schemas:
// ✓ Good - versioned with migration
const dataAtom = atom({}, 'dataAtom').extend(
withLocalStorage({
key: 'data',
version: 1,
migration: (record, version) => {
// Handle version changes
return record.data
},
})
)
// ✗ Bad - no version
const dataAtom = atom({}, 'dataAtom').extend(
withLocalStorage('data')
)
4. Set Appropriate TTLs
Don’t store data forever:
// ✓ Good - cache expires after 1 hour
const cacheAtom = atom(null, 'cacheAtom').extend(
withLocalStorage({
key: 'cache',
time: 1000 * 60 * 60, // 1 hour
})
)
// ✗ Bad - never expires (default: MAX_SAFE_TIMEOUT)
const cacheAtom = atom(null, 'cacheAtom').extend(
withLocalStorage('cache')
)
Security Warning: Never persist sensitive data like passwords or auth tokens in localStorage or sessionStorage. Use httpOnly cookies or secure server-side sessions instead.
Common Patterns
Persist Partial State
Only persist specific fields:
import { computed } from '@reatom/core'
const appStateAtom = atom({
user: { id: 1, name: 'John' },
theme: 'dark',
tempData: { /* ... */ },
}, 'appStateAtom')
// Only persist theme
const persistedThemeAtom = computed(
() => appStateAtom().theme,
'persistedThemeAtom'
).extend(withLocalStorage('theme'))
// Sync back to app state
appStateAtom.extend(
withInit(() => ({
...appStateAtom(),
theme: persistedThemeAtom(),
}))
)
Lazy Hydration
Defer hydration until needed:
import { atom } from '@reatom/core'
import { withInit } from '@reatom/core'
const settingsAtom = atom({}, 'settingsAtom')
const hydrateSettings = action(() => {
const stored = localStorage.getItem('settings')
if (stored) {
settingsAtom.set(JSON.parse(stored))
}
}, 'hydrateSettings')
// Hydrate on demand
effect(() => {
if (userLoggedIn()) {
hydrateSettings()
}
})
Conditional Persistence
Persist based on conditions:
import { effect } from '@reatom/core'
const dataAtom = atom({}, 'dataAtom')
const shouldPersist = atom(false, 'shouldPersist')
effect(() => {
if (shouldPersist()) {
localStorage.setItem('data', JSON.stringify(dataAtom()))
}
})
Encrypted Storage
Encrypt sensitive data:
import { reatomPersist } from '@reatom/core/persist'
import { encrypt, decrypt } from 'crypto-lib'
const withEncryptedStorage = reatomPersist({
name: 'encrypted',
cache: new Map(),
get({ key }) {
const encrypted = localStorage.getItem(key)
if (!encrypted) return null
const decrypted = decrypt(encrypted)
return JSON.parse(decrypted)
},
set({ key }, record) {
const json = JSON.stringify(record)
const encrypted = encrypt(json)
localStorage.setItem(key, encrypted)
},
})
const sensitiveAtom = atom({}, 'sensitiveAtom').extend(
withEncryptedStorage('sensitive-data')
)
Testing
Mock Storage
import { createMemStorage, reatomPersist } from '@reatom/core/persist'
import { test, expect } from 'vitest'
test('persists state', () => {
const storage = createMemStorage({ name: 'test' })
const withTestStorage = reatomPersist(storage)
const counterAtom = atom(0, 'counterAtom').extend(
withTestStorage('counter')
)
counterAtom.set(5)
// Check storage
const record = storage.get({ key: 'counter' })
expect(record?.data).toBe(5)
})
Reset Between Tests
import { beforeEach } from 'vitest'
beforeEach(() => {
localStorage.clear()
sessionStorage.clear()
})