🏗️ Advanced Patterns
Production-ready patterns for user data: caching, versioning, migrations, and optimistic updates.
Pattern Overview
These patterns help you build robust applications that handle real-world scenarios like slow networks, offline usage, and data structure changes.
Local Caching
Reduce API calls with localStorage caching
Optimistic Updates
Instant UI feedback while saving
Data Versioning
Track and migrate data structure changes
Merge Strategy
Handle concurrent updates gracefully
Pattern 1: UserDataManager Class
A complete data manager that handles caching, saving, and provides a clean API for your app:
class UserDataManager {
constructor(defaults = {}) {
this.defaults = defaults;
this.cache = null;
this.cacheKey = 'gammaltech_userdata_cache';
this.listeners = [];
}
// Get data with caching
async get() {
// Return cache if available
if (this.cache) return this.cache;
// Try localStorage first (offline support)
const cached = localStorage.getItem(this.cacheKey);
if (cached) {
this.cache = JSON.parse(cached);
}
// Fetch fresh data if logged in
if (GammalTech.isLoggedIn()) {
try {
const remote = await GammalTech.user.get();
this.cache = { ...this.defaults, ...remote };
this._saveToLocalStorage();
} catch (e) {
console.warn('Failed to fetch remote data', e);
}
}
return this.cache || this.defaults;
}
// Update specific fields
async update(updates) {
const current = await this.get();
const updated = { ...current, ...updates };
return this.save(updated);
}
// Save with optimistic update
async save(data) {
// Optimistic: update cache immediately
this.cache = data;
this._saveToLocalStorage();
this._notifyListeners();
// Save to server
if (GammalTech.isLoggedIn()) {
try {
await GammalTech.user.save(data);
} catch (e) {
console.error('Failed to save to server', e);
// Data is still in localStorage, will sync later
}
}
return data;
}
// Subscribe to changes
subscribe(callback) {
this.listeners.push(callback);
return () => {
this.listeners = this.listeners.filter(l => l !== callback);
};
}
// Clear cache (on logout)
clear() {
this.cache = null;
localStorage.removeItem(this.cacheKey);
}
_saveToLocalStorage() {
localStorage.setItem(this.cacheKey, JSON.stringify(this.cache));
}
_notifyListeners() {
this.listeners.forEach(cb => cb(this.cache));
}
}
// Usage
const userData = new UserDataManager({
theme: 'light',
language: 'en'
});
// Get data
const data = await userData.get();
// Update single field
await userData.update({ theme: 'dark' });
// Subscribe to changes
userData.subscribe((data) => {
console.log('Data changed:', data);
});
Pattern 2: Data Versioning & Migration
When your data structure evolves, use versioning to migrate old data automatically:
const CURRENT_VERSION = 3;
const migrations = {
// v1 -> v2: Renamed 'darkMode' to 'theme'
1: (data) => ({
...data,
theme: data.darkMode ? 'dark' : 'light',
darkMode: undefined
}),
// v2 -> v3: Added settings object
2: (data) => ({
...data,
settings: {
theme: data.theme || 'light',
language: data.language || 'en'
},
theme: undefined,
language: undefined
})
};
async function getDataWithMigration() {
let data = await GammalTech.user.get();
const version = data._version || 1;
// Run migrations if needed
if (version < CURRENT_VERSION) {
for (let v = version; v < CURRENT_VERSION; v++) {
if (migrations[v]) {
data = migrations[v](data);
console.log(`Migrated from v${v} to v${v + 1}`);
}
}
// Save migrated data
data._version = CURRENT_VERSION;
await GammalTech.user.save(data);
}
return data;
}
// Always save with version
async function saveData(data) {
data._version = CURRENT_VERSION;
await GammalTech.user.save(data);
}
Always keep old migration functions even after all users have migrated. A user who hasn't logged in for months might still have v1 data.
Pattern 3: Debounced Auto-Save
For frequently changing data (like form inputs), debounce saves to avoid excessive API calls:
class AutoSaveManager {
constructor(delay = 1000) {
this.delay = delay;
this.timeout = null;
this.pendingData = null;
this.isSaving = false;
}
// Queue data for saving
save(data) {
this.pendingData = data;
// Clear existing timeout
if (this.timeout) {
clearTimeout(this.timeout);
}
// Schedule save
this.timeout = setTimeout(() => {
this._doSave();
}, this.delay);
// Update UI to show "saving..."
this._showSavingIndicator();
}
// Force immediate save
async flush() {
if (this.timeout) {
clearTimeout(this.timeout);
}
if (this.pendingData) {
await this._doSave();
}
}
async _doSave() {
if (!this.pendingData || this.isSaving) return;
this.isSaving = true;
const dataToSave = this.pendingData;
this.pendingData = null;
try {
await GammalTech.user.save(dataToSave);
this._showSavedIndicator();
} catch (e) {
console.error('Auto-save failed', e);
this._showErrorIndicator();
} finally {
this.isSaving = false;
}
}
_showSavingIndicator() {
document.getElementById('saveStatus').textContent = 'Saving...';
}
_showSavedIndicator() {
document.getElementById('saveStatus').textContent = '✓ Saved';
}
_showErrorIndicator() {
document.getElementById('saveStatus').textContent = '⚠ Save failed';
}
}
// Usage
const autoSave = new AutoSaveManager(1500); // 1.5s delay
// On every form change
document.getElementById('bio').addEventListener('input', async (e) => {
const current = await GammalTech.user.get();
current.bio = e.target.value;
autoSave.save(current);
});
// Before leaving page
window.addEventListener('beforeunload', () => {
autoSave.flush();
});
Pattern 4: Deep Nested Updates
Safely update deeply nested properties without losing other data:
// Deep merge utility
function deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = deepMerge(result[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
// Update at path (like "settings.notifications.email")
function setAtPath(obj, path, value) {
const parts = path.split('.');
const result = { ...obj };
let current = result;
for (let i = 0; i < parts.length - 1; i++) {
current[parts[i]] = { ...current[parts[i]] };
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
return result;
}
// Usage examples
async function updateNestedSetting(path, value) {
const data = await GammalTech.user.get();
const updated = setAtPath(data, path, value);
await GammalTech.user.save(updated);
}
// Update just email notifications
await updateNestedSetting('settings.notifications.email', false);
// Merge multiple nested changes
const data = await GammalTech.user.get();
const updated = deepMerge(data, {
settings: {
theme: 'dark',
notifications: {
push: true
}
}
});
await GammalTech.user.save(updated);
Pattern 5: Managing Collections
Add, remove, and update items in arrays (favorites, cart, history):
class UserCollection {
constructor(collectionName) {
this.name = collectionName;
}
async getAll() {
const data = await GammalTech.user.get();
return data[this.name] || [];
}
async add(item) {
const data = await GammalTech.user.get();
const collection = data[this.name] || [];
// Add with timestamp
collection.push({
...item,
_id: Date.now().toString(),
_addedAt: new Date().toISOString()
});
data[this.name] = collection;
await GammalTech.user.save(data);
return collection;
}
async remove(itemId) {
const data = await GammalTech.user.get();
data[this.name] = (data[this.name] || []).filter(i => i._id !== itemId);
await GammalTech.user.save(data);
return data[this.name];
}
async update(itemId, updates) {
const data = await GammalTech.user.get();
data[this.name] = (data[this.name] || []).map(item =>
item._id === itemId ? { ...item, ...updates } : item
);
await GammalTech.user.save(data);
return data[this.name];
}
async has(predicate) {
const items = await this.getAll();
return items.some(predicate);
}
}
// Usage: Favorites collection
const favorites = new UserCollection('favorites');
// Add to favorites
await favorites.add({ productId: 'prod_123', name: 'Cool Product' });
// Check if favorited
const isFav = await favorites.has(f => f.productId === 'prod_123');
// Remove from favorites
await favorites.remove('1704450000000');
// Get all favorites
const allFavs = await favorites.getAll();
Pattern 6: Sync on Authentication
Merge local guest data with server data when user logs in:
const LOCAL_KEY = 'guest_data';
// Save locally when not logged in
function saveLocalData(data) {
localStorage.setItem(LOCAL_KEY, JSON.stringify(data));
}
// Get local guest data
function getLocalData() {
const stored = localStorage.getItem(LOCAL_KEY);
return stored ? JSON.parse(stored) : {};
}
// Handle login - merge local with remote
GammalTech.onAuth(
async (token) => {
// User logged in
const localData = getLocalData();
const remoteData = await GammalTech.user.get();
// Merge strategy: remote wins for conflicts, keep local additions
const merged = {
...localData, // Local as base
...remoteData, // Remote overwrites
// Special handling for arrays (combine, dedupe)
favorites: [
...(remoteData.favorites || []),
...(localData.favorites || []).filter(local =>
!(remoteData.favorites || []).some(r => r.productId === local.productId)
)
]
};
// Save merged data
await GammalTech.user.save(merged);
// Clear local storage
localStorage.removeItem(LOCAL_KEY);
console.log('Data synced!');
},
() => {
// User logged out - continue with local storage
console.log('Using local storage');
}
);
Best Practices Summary
• Cache data locally for offline support and speed
• Use versioning from day one
• Debounce frequent saves
• Handle merge conflicts gracefully
• Clear cache on logout
• Don't save on every keystroke without debouncing
• Don't assume data structure never changes
• Don't forget to handle offline scenarios
• Don't store sensitive data (passwords, tokens)
AI Prompt for Vibe Coding
Advanced PatternsCopy this prompt for help with advanced data patterns: