options.onSync - Cross-Tab Sync Event Callback
Quick Start (30 seconds)
const userState = state({ theme: 'light', notifications: true });
autoSave(userState, 'settings', {
sync: true,
onSync: (newData) => {
// Called when another tab changes the data
console.log('Settings synced from another tab:', newData);
showToast('Settings updated!');
}
});
// Open two tabs:
// Tab 1: Change theme to 'dark'
// Tab 2: onSync fires with { theme: 'dark', ... } ✨What just happened? You added a callback that fires whenever data is synced from another browser tab!
What is options.onSync?
options.onSync is a callback function that fires when data is synchronized from another browser tab/window.
Simply put: it's like a doorbell that rings when someone else makes a delivery. You're notified when data changes in another tab.
Think of it as a cross-tab notification system.
Syntax
autoSave(state, key, {
sync: true, // Must be enabled
onSync: (newData) => {
// Handle synced data
}
});Parameters:
newData- The new data that was synced from another tab
Returns:
- Nothing (void function)
Default: null (no callback)
Why Does This Exist?
The Challenge: Silent Updates
With sync enabled but no callback, updates happen silently:
const settings = state({ theme: 'light' });
autoSave(settings, 'settings', {
sync: true
// No onSync callback
});
// Tab 1: User changes theme
settings.theme = 'dark';
// Tab 2: Theme updates automatically BUT
// - User doesn't know
// - No visual feedback
// - Confusing UX ❌What's the Real Issue?
Tab 1 changes data
|
v
Tab 2 receives update
|
v
State changes silently
|
v
User confused ❌Problems: ❌ Silent updates - No user feedback
❌ Confusing - UI changes without explanation
❌ No logging - Can't track sync events
❌ No custom handling - Can't add special logic
The Solution with options.onSync
const settings = state({ theme: 'light' });
autoSave(settings, 'settings', {
sync: true,
onSync: (newData) => {
// Notify user
showToast(`Settings updated: ${newData.theme} theme`);
// Log event
console.log('Synced from another tab', newData);
// Custom handling
if (newData.theme === 'dark') {
playSound('theme-change.mp3');
}
}
});
// Tab 2 user sees: "Settings updated: dark theme" ✨What Just Happened?
Tab 1 changes data
|
v
Tab 2 receives update
|
v
onSync callback fires
|
v
Show notification ✅
Custom logic runs ✅
User informed ✅Benefits: ✅ User feedback - Visual notification of changes
✅ Custom logic - Add special handling
✅ Logging - Track sync events
✅ Better UX - Users understand what happened
Mental Model
Think of sync without onSync as surprise room makeover:
No onSync (Silent Update)
┌─────────────────────┐
│ You leave room │
│ │
│ Someone redecorates│
│ │
│ You return │
│ "Why is this │
│ different??" ❌ │
└─────────────────────┘Think of onSync as notification of changes:
With onSync (You're Informed)
┌─────────────────────┐
│ You leave room │
│ │
│ Someone redecorates│
│ │
│ You get text: 📱 │
│ "Changed to blue │
│ theme!" │
│ │
│ You return │
│ "Oh cool!" ✅ │
└─────────────────────┘Key Insight: onSync turns silent updates into communicated changes.
How Does It Work?
The onSync callback is triggered by the storage event:
The Sync Flow
Tab 1: state.theme = 'dark'
|
v
Save to localStorage
|
v
Browser fires 'storage' event
|
v
Tab 2: Event listener catches it
|
v
Update local state
|
v
Call onSync(newData) ✨
|
v
Your custom logic runsImplementation
// Inside autoSave with sync enabled
window.addEventListener('storage', (event) => {
if (event.key === storageKey) {
const newData = JSON.parse(event.newValue);
// Update state
Object.assign(state, newData);
// Call onSync callback
if (options.onSync) {
options.onSync(newData);
}
}
});Basic Usage
Example 1: Toast Notification
const settings = state({ theme: 'light' });
autoSave(settings, 'settings', {
sync: true,
onSync: (data) => {
showToast('Settings synced from another tab');
}
});Example 2: Console Logging
const userState = state({ name: '', status: '' });
autoSave(userState, 'user', {
sync: true,
onSync: (data) => {
console.log('User data synced:', data);
console.log('Updated at:', new Date().toLocaleTimeString());
}
});Example 3: Conditional Actions
const cart = state({ items: [] });
autoSave(cart, 'cart', {
sync: true,
onSync: (data) => {
if (data.items.length > cart.items.length) {
showNotification('Item added to cart in another tab');
} else if (data.items.length < cart.items.length) {
showNotification('Item removed from cart');
}
}
});Real-World Examples
Example 1: Authentication Sync
const auth = state({
isLoggedIn: false,
user: null
});
autoSave(auth, 'auth', {
sync: true,
onSync: (data) => {
if (!data.isLoggedIn && auth.isLoggedIn) {
// User logged out in another tab
showDialog({
title: 'Logged Out',
message: 'You were logged out in another tab',
onOk: () => redirectToLogin()
});
} else if (data.isLoggedIn && !auth.isLoggedIn) {
// User logged in
showToast('Logged in from another tab');
loadUserData();
}
}
});Example 2: Shopping Cart Updates
const cart = state({ items: [], total: 0 });
autoSave(cart, 'cart', {
sync: true,
debounce: 500,
onSync: (data) => {
// Show indicator
const indicator = document.getElementById('sync-indicator');
indicator.textContent = '🔄 Cart updated';
indicator.classList.add('show');
// Calculate differences
const newItems = data.items.length - cart.items.length;
if (newItems > 0) {
showToast(`${newItems} item(s) added in another tab`);
}
// Hide indicator after 3 seconds
setTimeout(() => {
indicator.classList.remove('show');
}, 3000);
}
});Example 3: Collaborative Feature Flags
const features = state({
newUI: false,
betaFeatures: false
});
autoSave(features, 'features', {
sync: true,
onSync: (data) => {
// Check what changed
const changes = [];
Object.keys(data).forEach(key => {
if (data[key] !== features[key]) {
changes.push(`${key}: ${features[key]} → ${data[key]}`);
}
});
if (changes.length > 0) {
console.log('Feature flags updated:', changes);
if (confirm('Features updated. Reload to apply changes?')) {
window.location.reload();
}
}
}
});Example 4: Live Dashboard Sync
const dashboard = state({
widgets: [],
layout: {}
});
autoSave(dashboard, 'dashboard', {
sync: true,
onSync: (data) => {
// Show sync animation
document.body.classList.add('syncing');
// Log change
const timestamp = new Date().toLocaleTimeString();
const log = document.getElementById('sync-log');
log.insertAdjacentHTML('afterbegin',
`<div>${timestamp}: Dashboard synced</div>`
);
// Remove animation
setTimeout(() => {
document.body.classList.remove('syncing');
}, 500);
}
});Example 5: Notification Center
const notifications = state({ items: [] });
autoSave(notifications, 'notifications', {
sync: true,
onSync: (data) => {
// Check for new notifications
const oldCount = notifications.items.length;
const newCount = data.items.length;
if (newCount > oldCount) {
const newNotifs = newCount - oldCount;
// Show notification
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('New Notification', {
body: `You have ${newNotifs} new notification(s)`,
icon: '/icon.png'
});
}
// Play sound
const audio = new Audio('/notification.mp3');
audio.play();
// Update badge
document.getElementById('notif-badge').textContent = newNotifs;
}
}
});Common Patterns
Pattern 1: Sync Indicator
autoSave(state, 'data', {
sync: true,
onSync: (data) => {
const indicator = document.getElementById('sync-status');
// Show syncing
indicator.textContent = '🔄 Syncing...';
indicator.className = 'syncing';
// Change to synced after animation
setTimeout(() => {
indicator.textContent = '✓ Synced';
indicator.className = 'synced';
// Hide after 2 seconds
setTimeout(() => {
indicator.className = 'hidden';
}, 2000);
}, 500);
}
});Pattern 2: Track Sync History
const syncHistory = [];
autoSave(state, 'data', {
sync: true,
onSync: (data) => {
syncHistory.push({
timestamp: Date.now(),
data: JSON.parse(JSON.stringify(data))
});
// Keep last 10 syncs
if (syncHistory.length > 10) {
syncHistory.shift();
}
console.log('Sync history:', syncHistory);
}
});Pattern 3: Conflict Detection
let lastModified = Date.now();
autoSave(state, 'data', {
sync: true,
onSync: (data) => {
const timeSinceLastEdit = Date.now() - lastModified;
if (timeSinceLastEdit < 5000) {
// Recent local edit - potential conflict
console.warn('Potential conflict detected');
if (confirm('Data changed in another tab. Accept changes?')) {
// Accept synced data
lastModified = Date.now();
} else {
// Keep local data
save(myState);
}
} else {
// No conflict - accept sync
lastModified = Date.now();
}
}
});Pattern 4: Selective Reload
autoSave(state, 'config', {
sync: true,
onSync: (data) => {
// Check what changed
const criticalFields = ['apiUrl', 'apiKey'];
const changed = criticalFields.some(field =>
data[field] !== state[field]
);
if (changed) {
showWarning('Critical settings changed. Page will reload in 3 seconds...');
setTimeout(() => {
window.location.reload();
}, 3000);
} else {
showToast('Settings updated');
}
}
});Pattern 5: Analytics Tracking
autoSave(state, 'data', {
sync: true,
onSync: (data) => {
// Track sync event
analytics.track('cross_tab_sync', {
dataType: 'user_settings',
timestamp: Date.now(),
itemCount: Object.keys(data).length
});
}
});Pattern 6: Rate Limiting
let lastSyncNotification = 0;
const NOTIFICATION_COOLDOWN = 5000; // 5 seconds
autoSave(state, 'data', {
sync: true,
onSync: (data) => {
const now = Date.now();
if (now - lastSyncNotification > NOTIFICATION_COOLDOWN) {
showToast('Data synchronized');
lastSyncNotification = now;
}
// Always log, even if notification suppressed
console.log('Sync received', data);
}
});Important Notes
1. Only Fires in Other Tabs
// ⚠️ onSync does NOT fire in the tab that made the change
// Tab 1: Changes data
state.value = 'new';
// onSync does NOT fire in Tab 1
// Tab 2, 3, etc: Receive the sync
// onSync DOES fire in Tab 2, 3, etc.2. Requires sync: true
// ❌ Won't work - sync not enabled
autoSave(state, 'data', {
onSync: (data) => console.log(data)
});
// ✅ Correct - sync enabled
autoSave(state, 'data', {
sync: true,
onSync: (data) => console.log(data)
});3. State Already Updated
autoSave(state, 'data', {
sync: true,
onSync: (newData) => {
// State is already updated by the time onSync fires
console.log(state.value === newData.value); // true
// onSync is for notifications, not for updating state
// (state is already updated automatically)
}
});Summary
What is options.onSync?
A callback function that fires when data is synchronized from another browser tab.
Why use it?
- ✅ Notify users of changes
- ✅ Log sync events
- ✅ Add custom logic on sync
- ✅ Handle conflicts
- ✅ Better user experience
Key Takeaway:
Without onSync With onSync
| |
Silent updates Notified updates
| |
User confused ❌ User informed ✅One-Line Rule: Use onSync to handle and respond to cross-tab synchronization events.
Common Use Cases:
- Notifications: Toast messages for changes
- Logging: Track sync events
- Reloading: Reload page for critical changes
- Conflicts: Detect and resolve conflicts
- Analytics: Track cross-tab usage
Best Practices:
- Always enable
sync: true - Show visual feedback to users
- Rate limit notifications
- Log sync events for debugging
- Handle conflicts gracefully
Remember: onSync keeps users informed across tabs! 🎉