Persistence
QuantaJS provides a powerful persistence system that automatically saves and restores store state across browser sessions, with support for multiple storage adapters, data migrations, and cross-tab synchronization.
Overview
The persistence system consists of:
- Persistence Manager: Core functionality for managing persisted state
- Storage Adapters: Different storage backends (localStorage, sessionStorage, IndexedDB)
- Migration System: Handle schema changes across versions
- Cross-tab Sync: Synchronize state across multiple browser tabs
Core API
createPersistenceManager
Creates a persistence manager for a store with automatic state saving and loading.
function createPersistenceManager<T extends Record<string, any>>(
getState: () => T,
setState: (newState: Partial<T>) => void,
notifySubscribers: () => void,
config: PersistenceConfig<T>,
storeName?: string
): PersistenceManager
Parameters
getState
: Function that returns the current statesetState
: Function to update state with new valuesnotifySubscribers
: Function to notify store subscribers of changesconfig
: Persistence configuration objectstoreName
: Optional name for the store (used for debugging)
Returns
A PersistenceManager
instance with methods for manual persistence control.
Storage Adapters
LocalStorageAdapter
Persists data to browser's localStorage with cross-tab synchronization.
class LocalStorageAdapter implements PersistenceAdapter {
constructor(key: string)
}
Features:
- Automatic JSON serialization/deserialization
- Cross-tab synchronization via storage events
- Handles quota exceeded errors gracefully
SessionStorageAdapter
Persists data to browser's sessionStorage (cleared when tab closes).
class SessionStorageAdapter implements PersistenceAdapter {
constructor(key: string)
}
Features:
- Data persists only for the current browser session
- Cross-tab synchronization
- Useful for temporary state that shouldn't persist across sessions
IndexedDBAdapter
Persists data to IndexedDB for larger datasets and better performance.
class IndexedDBAdapter implements PersistenceAdapter {
constructor(
key: string,
dbName?: string,
storeName?: string,
version?: number
)
}
Features:
- Handles large amounts of data efficiently
- Asynchronous operations
- Better performance for complex data structures
Configuration Options
PersistenceConfig
interface PersistenceConfig<T = any> {
adapter: PersistenceAdapter;
serialize?: (data: T) => string;
deserialize?: (data: string) => T;
migrations?: Record<number, (data: any) => any>;
version?: number;
debounceMs?: number;
include?: Array<keyof T>;
exclude?: Array<keyof T>;
transform?: {
in?: (data: any) => any;
out?: (data: any) => any;
};
onError?: (error: Error, operation: 'read' | 'write' | 'remove') => void;
validator?: (data: any) => boolean;
}
Configuration Properties
adapter
: Storage adapter to use (required)serialize
: Custom serialization function (defaults toJSON.stringify
)deserialize
: Custom deserialization function (defaults toJSON.parse
)migrations
: Version migration functions for schema changesversion
: Current data schema version (defaults to 1)debounceMs
: Debounce delay for save operations (defaults to 300ms)include
: Array of state properties to persist (if not specified, all properties are persisted)exclude
: Array of state properties to exclude from persistencetransform
: Data transformation functions for input/outputonError
: Error handler for persistence operationsvalidator
: Function to validate data before saving/after loading
Migration System
MigrationManager
Handles schema changes across different versions of your data.
class MigrationManager<T = any> {
constructor(migrations: MigrationDefinition<T>[] = [])
addMigration(version: number, migrate: MigrationFunction<T>): void
removeMigration(version: number): void
getVersions(): number[]
hasMigration(version: number): boolean
getHighestVersion(): number
migrate(data: any, fromVersion: number, toVersion: number): any
validateMigrations(): { valid: boolean; errors: string[] }
}
Common Migration Patterns
import { CommonMigrations } from '@quantajs/core';
const migrations = {
2: CommonMigrations.addProperty('settings', { theme: 'light' }),
3: CommonMigrations.renameProperty('user', 'currentUser'),
4: CommonMigrations.transformProperty('todos', (todos) =>
todos.map(todo => ({ ...todo, priority: 'medium' }))
),
5: CommonMigrations.removeProperty('deprecatedField'),
};
Persistence Manager
PersistenceManager
Interface for controlling persistence operations.
interface PersistenceManager {
save(): Promise<void>;
load(): Promise<void>;
clear(): Promise<void>;
getAdapter(): PersistenceAdapter;
isRehydrated(): boolean;
}
Methods
save()
: Manually trigger a save operationload()
: Manually trigger a load operationclear()
: Remove all persisted datagetAdapter()
: Get the underlying storage adapterisRehydrated()
: Check if initial data has been loaded
Basic Usage
Simple Persistence
import { createStore, LocalStorageAdapter } from '@quantajs/core';
const store = createStore('user', {
state: () => ({
name: '',
email: '',
preferences: { theme: 'light' }
}),
persist: {
adapter: new LocalStorageAdapter('user-store'),
version: 1,
debounceMs: 500
}
});
Advanced Persistence with Migrations
import { createStore, LocalStorageAdapter, CommonMigrations } from '@quantajs/core';
const store = createStore('app', {
state: () => ({
user: null,
settings: { theme: 'light', language: 'en' },
todos: []
}),
persist: {
adapter: new LocalStorageAdapter('app-store'),
version: 3,
debounceMs: 300,
include: ['user', 'settings'], // Only persist user and settings
exclude: ['todos'], // Explicitly exclude todos
migrations: {
2: CommonMigrations.addProperty('settings', { language: 'en' }),
3: CommonMigrations.renameProperty('theme', 'appTheme')
},
onError: (error, operation) => {
console.error(`Persistence ${operation} failed:`, error);
}
}
});
Custom Serialization
const store = createStore('complex', {
state: () => ({
dates: [new Date()],
functions: [() => console.log('test')]
}),
persist: {
adapter: new LocalStorageAdapter('complex-store'),
serialize: (data) => {
// Custom serialization to handle Date objects
return JSON.stringify(data, (key, value) => {
if (value instanceof Date) {
return { __type: 'Date', value: value.toISOString() };
}
return value;
});
},
deserialize: (data) => {
// Custom deserialization to restore Date objects
return JSON.parse(data, (key, value) => {
if (value && value.__type === 'Date') {
return new Date(value.value);
}
return value;
});
}
}
});
Data Validation
const store = createStore('validated', {
state: () => ({
count: 0,
name: ''
}),
persist: {
adapter: new LocalStorageAdapter('validated-store'),
validator: (data) => {
// Ensure count is a number and name is a string
return typeof data.count === 'number' &&
typeof data.name === 'string' &&
data.count >= 0;
},
onError: (error, operation) => {
if (operation === 'read') {
// If validation fails on read, reset to defaults
store.$reset();
}
}
}
});
Cross-tab Synchronization
All storage adapters support cross-tab synchronization automatically. When data changes in one tab, other tabs are automatically updated:
// Tab 1
store.name = 'John';
// Tab 2 automatically receives the update
console.log(store.name); // 'John'
Error Handling
The persistence system provides comprehensive error handling:
const store = createStore('robust', {
state: () => ({ data: [] }),
persist: {
adapter: new LocalStorageAdapter('robust-store'),
onError: (error, operation) => {
switch (operation) {
case 'read':
console.warn('Failed to load persisted state, using defaults');
break;
case 'write':
console.error('Failed to save state:', error);
// Optionally show user notification
break;
case 'remove':
console.error('Failed to clear persisted data:', error);
break;
}
}
}
});
Performance Considerations
Debouncing
The persistence system automatically debounces save operations to prevent excessive writes:
const store = createStore('performance', {
state: () => ({ counter: 0 }),
persist: {
adapter: new LocalStorageAdapter('performance-store'),
debounceMs: 1000 // Save only after 1 second of inactivity
}
});
// Rapid changes will be batched
for (let i = 0; i < 100; i++) {
store.counter++;
}
// Only one save operation will occur after 1 second
Selective Persistence
Only persist necessary data to improve performance:
const store = createStore('selective', {
state: () => ({
user: null,
temporaryData: [],
cache: new Map()
}),
persist: {
adapter: new LocalStorageAdapter('selective-store'),
include: ['user'], // Only persist user data
exclude: ['temporaryData', 'cache'] // Explicitly exclude large objects
}
});
Learn More
- Persistence Guide - Comprehensive guide to using persistence
- Storage Adapters - Detailed adapter documentation
- Best Practices - Persistence best practices