Persistence Guide
These examples use `createStore`:
createStore builds a store instance immediately and is still fully supported.
In application code prefer defineStore, which
returns an accessor you call — it is safe at module scope on a server and needs
no container passed around. Everything on this page applies to both.
QuantaJS persistence system allows you to automatically save and restore store state across browser sessions, providing a seamless user experience and offline functionality.
Getting Started
Basic Persistence Setup
The simplest way to add persistence to your store is by adding a persist option:
import { createStore, LocalStorageAdapter } from '@quantajs/core';
const userStore = createStore('user', {
state: () => ({
name: '',
email: '',
preferences: { theme: 'light' }
}),
persist: {
adapter: new LocalStorageAdapter('user-store')
}
});
This automatically:
- Saves state changes to localStorage
- Restores state when the page reloads
- Synchronizes data across browser tabs
- Handles serialization/deserialization
Understanding Storage Adapters
QuantaJS provides four storage adapters for different use cases:
LocalStorage Adapter
Best for most applications - data persists until manually cleared:
import { LocalStorageAdapter } from '@quantajs/core';
const store = createStore('app', {
state: () => ({ settings: { theme: 'light' } }),
persist: {
adapter: new LocalStorageAdapter('app-settings')
}
});
SessionStorage Adapter
Data persists only for the current browser session:
import { SessionStorageAdapter } from '@quantajs/core';
const store = createStore('session', {
state: () => ({ temporaryData: [] }),
persist: {
adapter: new SessionStorageAdapter('session-data')
}
});
IndexedDB Adapter
Best for large datasets and complex data structures:
import { IndexedDBAdapter } from '@quantajs/core';
const store = createStore('large', {
state: () => ({ documents: [] }),
persist: {
adapter: new IndexedDBAdapter('documents', 'app-db', 'stores', 1)
}
});
Cookie Adapter
For a small piece of state the server should also see, such as a theme or locale. The cookie travels with every request, so persist only what the server needs:
import { CookieAdapter } from '@quantajs/core';
const store = createStore('prefs', {
state: () => ({ theme: 'light', recentSearches: [] }),
persist: {
adapter: new CookieAdapter('prefs', { maxAge: 60 * 60 * 24 * 365 }),
include: ['theme'],
}
});
A cookie holds at most 4096 bytes. A write that would exceed that throws, and the store reports it through onError while the previous cookie stays in place.
Advanced Configuration
Selective Persistence
Control which parts of your state are persisted:
const store = createStore('selective', {
state: () => ({
user: null,
settings: { theme: 'light' },
temporaryData: [],
cache: new Map()
}),
persist: {
adapter: new LocalStorageAdapter('selective-store'),
include: ['user', 'settings'], // Only persist these properties
exclude: ['temporaryData', 'cache'] // Explicitly exclude these
}
});
Custom Serialization
Handle complex data types that can't be serialized with JSON. serialize receives the whole stored envelope, { data, version, timestamp, storeName }, not only the state, and deserialize must return that shape. A JSON replacer and reviver pair, as here, handles that naturally.
const store = createStore('complex', {
state: () => ({
dates: [new Date()],
functions: [() => console.log('test')],
customObjects: new CustomClass()
}),
persist: {
adapter: new LocalStorageAdapter('complex-store'),
serialize: (data) => {
return JSON.stringify(data, (key, value) => {
if (value instanceof Date) {
return { __type: 'Date', value: value.toISOString() };
}
if (value instanceof CustomClass) {
return { __type: 'CustomClass', data: value.serialize() };
}
if (typeof value === 'function') {
return { __type: 'Function', name: value.name };
}
return value;
});
},
deserialize: (data) => {
return JSON.parse(data, (key, value) => {
if (value && value.__type === 'Date') {
return new Date(value.value);
}
if (value && value.__type === 'CustomClass') {
return CustomClass.deserialize(value.data);
}
return value;
});
}
}
});
Data Validation
Ensure data integrity with validation functions:
const store = createStore('validated', {
state: () => ({
user: null,
settings: { theme: 'light', language: 'en' }
}),
persist: {
adapter: new LocalStorageAdapter('validated-store'),
validator: (data) => {
// Validate user data
if (data.user && typeof data.user.id !== 'string') {
return false;
}
// Validate settings
if (data.settings) {
const validThemes = ['light', 'dark', 'auto'];
const validLanguages = ['en', 'es', 'fr'];
if (!validThemes.includes(data.settings.theme)) {
return false;
}
if (!validLanguages.includes(data.settings.language)) {
return false;
}
}
return true;
},
onError: (error, operation) => {
// Stored data that fails the validator is not loaded, so the store
// keeps its defaults. Remove it so the next visit starts clean.
if (operation === 'read') store.$persist?.clear();
}
}
});
Migration System
Why Migrations?
As your application evolves, your data structure may change. Migrations allow you to update existing persisted data to match new schemas:
const store = createStore('evolving', {
state: () => ({
user: null,
settings: { theme: 'light', language: 'en', notifications: true }
}),
persist: {
adapter: new LocalStorageAdapter('evolving-store'),
version: 3,
migrations: {
2: (data) => {
// Version 2: Add language setting
return {
...data,
settings: {
...data.settings,
language: 'en' // Default value for existing users
}
};
},
3: (data) => {
// Version 3: Add notifications setting
return {
...data,
settings: {
...data.settings,
notifications: true // Default value for existing users
}
};
}
}
}
});
Common Migration Patterns
A migration is a plain function from the data as stored under the previous version to the new shape. The usual changes take a line or two each:
const store = createStore('patterns', {
state: () => ({
currentUser: null,
settings: { theme: 'light', language: 'en' }
}),
persist: {
adapter: new LocalStorageAdapter('patterns-store'),
version: 4,
migrations: {
// 1 → 2: add a property
2: (data) => ({ ...data, settings: { language: 'en', ...data.settings } }),
// 2 → 3: rename a property
3: ({ user, ...rest }) => ({ ...rest, currentUser: user }),
// 3 → 4: transform a property
4: (data) => ({
...data,
settings: {
...data.settings,
theme: data.settings.theme === 'auto' ? 'light' : data.settings.theme
}
})
}
}
});
Custom Migration Functions
For complex migrations, create custom functions:
const store = createStore('custom', {
state: () => ({
todos: [],
categories: []
}),
persist: {
adapter: new LocalStorageAdapter('custom-store'),
version: 2,
migrations: {
2: (data) => {
// Complex migration: restructure todos with categories
if (data.todos && Array.isArray(data.todos)) {
const categories = new Set();
// Extract categories from existing todos
data.todos.forEach(todo => {
if (todo.category) {
categories.add(todo.category);
}
});
// Create categories array
data.categories = Array.from(categories).map(name => ({
id: Date.now() + Math.random(),
name,
color: '#007bff'
}));
// Update todos to reference category IDs
data.todos = data.todos.map(todo => {
if (todo.category) {
const category = data.categories.find(c => c.name === todo.category);
return {
...todo,
categoryId: category?.id || null,
category: undefined // Remove old property
};
}
return todo;
});
}
return data;
}
}
}
});
Performance Optimization
Debouncing Saves
Prevent excessive storage writes by debouncing save operations:
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 into a single save
for (let i = 0; i < 100; i++) {
store.counter++;
}
// Only one save operation occurs after 1 second
Selective Persistence
Only persist essential data to improve performance:
const store = createStore('efficient', {
state: () => ({
user: null,
settings: { theme: 'light' },
temporaryData: [],
largeCache: new Map(),
analytics: []
}),
persist: {
adapter: new LocalStorageAdapter('efficient-store'),
include: ['user', 'settings'], // Only persist critical data
exclude: ['temporaryData', 'largeCache', 'analytics'] // Exclude large objects
}
});
Lazy Loading
Load persisted data only when needed:
const store = createStore('lazy', {
state: () => ({
user: null,
isHydrated: false
}),
persist: {
adapter: new LocalStorageAdapter('lazy-store'),
onError: (error, operation) => {
if (operation === 'read') {
store.isHydrated = true; // Mark as hydrated even on error
}
}
}
});
// Wait for restoration to finish.
await store.$hydrated;
console.log('Store has been hydrated from storage');
$hydrated is a promise, so there is nothing to poll. It resolves immediately
when the store has no persistence configured, which means calling code does not
have to branch on whether persistence is on. For a synchronous check,
store.$persist?.isRehydrated() is still available.
Cross-tab Synchronization
Automatic Synchronization
The LocalStorageAdapter automatically synchronizes data across browser tabs via storage events:
const store = createStore('sync', {
state: () => ({ theme: 'light' }),
persist: {
adapter: new LocalStorageAdapter('sync-store')
}
});
// Tab 1
store.theme = 'dark';
// Tab 2 automatically receives the update
console.log(store.theme); // 'dark'
Note: Cross-tab synchronization is only supported with
LocalStorageAdapter.SessionStorageAdapterdoes not support it becausestorageevents do not fire for same-tabsessionStoragechanges.IndexedDBAdapterdoes not have a built-in change notification mechanism.
Manual Synchronization
For more control, you can manually trigger synchronization:
const store = createStore('manual-sync', {
state: () => ({ data: [] }),
persist: {
adapter: new LocalStorageAdapter('manual-sync-store')
}
});
// Force a save and sync
await store.$persist.save();
// Force a reload from storage
await store.$persist.load();
Error Handling
Comprehensive Error Handling
Handle different types of persistence errors:
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');
// Optionally show user notification
break;
case 'write':
console.error('Failed to save state:', error);
if (error.name === 'QuotaExceededError') {
// Handle storage quota exceeded
alert('Storage space exceeded. Some data may not be saved.');
}
break;
case 'remove':
console.error('Failed to clear persisted data:', error);
break;
}
}
}
});
Fallback Strategies
Implement fallback strategies when persistence fails:
const store = createStore('fallback', {
state: () => ({ settings: { theme: 'light' } }),
persist: {
adapter: new LocalStorageAdapter('fallback-store'),
onError: async (error, operation) => {
if (operation === 'read') {
// Try to load from a different storage method
try {
const fallbackData = sessionStorage.getItem('fallback-settings');
if (fallbackData) {
const parsed = JSON.parse(fallbackData);
Object.assign(store.settings, parsed);
}
} catch (fallbackError) {
console.warn('Fallback loading also failed:', fallbackError);
}
}
}
}
});
Real-world Examples
User Preferences Store
const userPreferencesStore = createStore('preferences', {
state: () => ({
theme: 'light',
language: 'en',
notifications: {
email: true,
push: false,
sms: false
},
accessibility: {
fontSize: 'medium',
highContrast: false,
reduceMotion: false
}
}),
persist: {
adapter: new LocalStorageAdapter('user-preferences'),
version: 2,
debounceMs: 500,
migrations: {
2: (data) => ({
...data,
accessibility: {
fontSize: 'medium',
highContrast: false,
reduceMotion: false,
...data.accessibility
}
})
},
onError: (error, operation) => {
if (operation === 'write') {
// Log to analytics service
analytics.track('persistence_error', { operation, error: error.message });
}
}
}
});
Shopping Cart Store
const cartStore = createStore('cart', {
state: () => ({
items: [],
appliedCoupons: [],
shippingAddress: null,
billingAddress: null
}),
persist: {
adapter: new LocalStorageAdapter('shopping-cart'),
version: 1,
debounceMs: 300,
include: ['items', 'appliedCoupons'], // Don't persist addresses
validator: (data) => {
// Ensure items have required properties
if (data.items && Array.isArray(data.items)) {
return data.items.every(item =>
item.id &&
typeof item.quantity === 'number' &&
item.quantity > 0
);
}
return true;
},
onError: (error, operation) => {
// Stored data that fails the validator is not loaded, so the store
// keeps its defaults. Remove it so the next visit starts clean.
if (operation === 'read') store.$persist?.clear();
}
}
});
Application State Store
const appStateStore = createStore('app-state', {
state: () => ({
currentRoute: '/',
sidebarOpen: false,
modals: [],
notifications: [],
user: null,
permissions: []
}),
persist: {
adapter: new LocalStorageAdapter('app-state'),
version: 1,
debounceMs: 1000,
include: ['currentRoute', 'sidebarOpen', 'user'], // Only persist essential UI state
exclude: ['modals', 'notifications', 'permissions'], // Don't persist temporary data
onError: (error, operation) => {
// Log errors for debugging
console.error(`App state persistence ${operation} failed:`, error);
// Don't break the app on persistence errors
if (operation === 'read') {
// Use default values
store.currentRoute = '/';
store.sidebarOpen = false;
}
}
}
});
Testing Persistence
Unit Testing
Test persistence functionality in isolation:
import { createStore, LocalStorageAdapter } from '@quantajs/core';
describe('Persistence', () => {
let store;
beforeEach(() => {
// Clear storage before each test
localStorage.clear();
store = createStore('test', {
state: () => ({ count: 0 }),
persist: {
adapter: new LocalStorageAdapter('test-store')
}
});
});
test('should persist state changes', async () => {
store.count = 42;
// Wait for debounced save
await new Promise(resolve => setTimeout(resolve, 400));
// Create new store instance to test loading
const newStore = createStore('test2', {
state: () => ({ count: 0 }),
persist: {
adapter: new LocalStorageAdapter('test-store')
}
});
expect(newStore.count).toBe(42);
});
test('should handle storage errors gracefully', () => {
// Mock localStorage to throw error
const originalSetItem = localStorage.setItem;
localStorage.setItem = () => { throw new Error('Storage error'); };
expect(() => {
store.count = 100;
}).not.toThrow();
// Restore original method
localStorage.setItem = originalSetItem;
});
});
Integration Testing
Test persistence with real storage:
describe('Persistence Integration', () => {
test('should sync across multiple store instances', async () => {
const store1 = createStore('sync-test', {
state: () => ({ data: 'initial' }),
persist: {
adapter: new LocalStorageAdapter('sync-test-store')
}
});
const store2 = createStore('sync-test2', {
state: () => ({ data: 'initial' }),
persist: {
adapter: new LocalStorageAdapter('sync-test-store')
}
});
// Update first store
store1.data = 'updated';
// Wait for persistence
await new Promise(resolve => setTimeout(resolve, 400));
// Second store should have updated data
expect(store2.data).toBe('updated');
});
});
Best Practices
1. Choose the Right Storage Adapter
- LocalStorage: For most applications, user preferences, settings
- SessionStorage: For temporary data, form drafts, session state
- IndexedDB: For large datasets, complex objects, offline data
2. Use Selective Persistence
Only persist essential data to improve performance:
persist: {
adapter: new LocalStorageAdapter('store'),
include: ['essential', 'user', 'settings'],
exclude: ['temporary', 'cache', 'analytics']
}
3. Implement Proper Error Handling
Always provide error handlers for production applications:
persist: {
adapter: new LocalStorageAdapter('store'),
onError: (error, operation) => {
// Log error
console.error(`Persistence ${operation} failed:`, error);
// Show user notification if appropriate
if (operation === 'write') {
showNotification('Failed to save data', 'error');
}
}
}
4. Use Migrations for Schema Changes
When updating your data structure, always provide migrations:
persist: {
adapter: new LocalStorageAdapter('store'),
version: 2,
migrations: {
2: (data) => ({ ...data, newField: 'default' })
}
}
5. Test Persistence Thoroughly
Test persistence functionality in your test suite:
test('persistence works correctly', async () => {
store.data = 'test';
await store.$persist.save();
const newStore = createStore('test', {
state: () => ({ data: '' }),
persist: { adapter: new LocalStorageAdapter('test') }
});
expect(newStore.data).toBe('test');
});
Learn More
- Persistence API - Complete API reference
- Storage Adapters - Detailed adapter documentation
- Best Practices - Persistence best practices
- Managing Stores - Store management patterns
Spot something that needs improving?
Edit on GitHubEdit page