Cross-Tab Synchronization
What is Cross-Tab Sync?
Cross-tab synchronization allows you to automatically sync state between multiple browser tabs or windows of the same application. When a user updates data in one tab, all other tabs instantly reflect the same changes.
Quick Start
Enable cross-tab sync by adding a syncKey
to your signify configuration:
import { signify } from "react-signify";
const sCounter = signify(0, {
syncKey: "my-counter",
});
function Counter() {
const count = sCounter.use();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => sCounter.set(count + 1)}>Increment</button>
</div>
);
}
Result: When you click the button in one tab, the counter updates in all open tabs instantly! ✨
Practical Examples
1. Shopping Cart
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
const sCart = signify<CartItem[]>([], {
syncKey: "user-cart",
});
const cartActions = {
addItem: (item: CartItem) => {
sCart.set((prev) => {
prev.value.push(item);
});
},
removeItem: (id: number) => {
sCart.set((prev) => {
prev.value = prev.value.filter((item) => item.id !== id);
});
},
};
function CartBadge() {
const cart = sCart.use();
const itemCount = cart.reduce((sum, item) => sum + item.quantity, 0);
return <div className="cart-badge">🛒 {itemCount}</div>;
}
Benefit: Add items to cart in one tab, see the cart badge update everywhere!
2. User Authentication
interface User {
id: number;
name: string;
email: string;
}
const sCurrentUser = signify<User | null>(null, {
syncKey: "user-session",
});
const auth = {
login: async (email: string, password: string) => {
const user = await loginAPI(email, password);
sCurrentUser.set(user);
// User logged in across all tabs instantly
},
logout: () => {
sCurrentUser.set(null);
// User logged out from all tabs instantly
},
};
function UserMenu() {
const user = sCurrentUser.use();
if (!user) {
return <button onClick={() => auth.login("...", "...")}>Login</button>;
}
return (
<div>
Welcome, {user.name}!<button onClick={auth.logout}>Logout</button>
</div>
);
}
Benefit: Login/logout in one tab affects all tabs immediately.
3. Real-time Notifications
interface Notification {
id: string;
message: string;
type: "info" | "success" | "error";
timestamp: number;
}
const sNotifications = signify<Notification[]>([], {
syncKey: "app-notifications",
});
const notifications = {
add: (message: string, type: Notification["type"] = "info") => {
sNotifications.set((prev) => {
prev.value.unshift({
id: Date.now().toString(),
message,
type,
timestamp: Date.now(),
});
});
},
remove: (id: string) => {
sNotifications.set((prev) => {
prev.value = prev.value.filter((n) => n.id !== id);
});
},
};
function NotificationCenter() {
const notifs = sNotifications.use();
return (
<div className="notifications">
{notifs.map((notif) => (
<div key={notif.id} className={`notification ${notif.type}`}>
{notif.message}
<button onClick={() => notifications.remove(notif.id)}>×</button>
</div>
))}
</div>
);
}
Benefit: Notifications appear instantly in all tabs when triggered from any tab.
Advanced Patterns
1. Conflict Resolution
Handle simultaneous updates from multiple tabs:
interface TimestampedData {
value: string;
timestamp: number;
tabId: string;
}
const sDocument = signify<TimestampedData>(
{
value: "",
timestamp: Date.now(),
tabId: "",
},
{
syncKey: "shared-document",
}
);
const TAB_ID = `tab-${Date.now()}-${Math.random()}`;
const updateDocument = (newValue: string) => {
sDocument.set((prev) => {
const now = Date.now();
// Only update if this change is newer
if (now > prev.value.timestamp) {
prev.value.value = newValue;
prev.value.timestamp = now;
prev.value.tabId = TAB_ID;
}
});
};
2. Leader Election
Coordinate tasks across tabs:
const sTabLeader = signify(
{
leaderId: null as string | null,
candidates: [] as { id: string; timestamp: number }[],
},
{
syncKey: "tab-leadership",
}
);
const electLeader = () => {
const tabId = `tab-${Date.now()}`;
sTabLeader.set((prev) => {
// Add this tab as candidate
prev.value.candidates.push({ id: tabId, timestamp: Date.now() });
// Clean up old candidates
const now = Date.now();
prev.value.candidates = prev.value.candidates.filter(
(c) => now - c.timestamp < 10000
);
// Elect earliest candidate as leader
const sortedCandidates = prev.value.candidates.sort(
(a, b) => a.timestamp - b.timestamp
);
prev.value.leaderId = sortedCandidates[0]?.id || null;
});
};
Best Practices
✅ Do's
// ✅ Use descriptive, versioned sync keys
const sUserPrefs = signify(data, {
syncKey: "user-preferences-v2",
});
// ✅ Keep synced data small and focused
const sTheme = signify("light", {
syncKey: "app-theme",
});
// ✅ Handle sync gracefully in components
function SyncAwareComponent() {
const data = sSyncedData.use();
// Show indicator when data comes from another tab
const [wasUpdatedExternally, setWasUpdatedExternally] = useState(false);
useEffect(() => {
setWasUpdatedExternally(true);
const timer = setTimeout(() => setWasUpdatedExternally(false), 2000);
return () => clearTimeout(timer);
}, [data]);
return (
<div>
{wasUpdatedExternally && <span>📡 Updated from another tab</span>}
<div>{data}</div>
</div>
);
}
❌ Don'ts
// ❌ Don't use same syncKey for different data
const sUserData = signify(userData, { syncKey: "app-data" });
const sCartData = signify(cartData, { syncKey: "app-data" }); // Collision!
// ❌ Don't sync sensitive data
const sPassword = signify(password, {
syncKey: "user-password", // Security risk!
});
// ❌ Don't sync large objects
const sHugeDataset = signify(massiveArray, {
syncKey: "large-data", // Performance issue
});
When to Use Cross-Tab Sync
Use Case | Sync? | Why |
---|---|---|
Shopping cart | ✅ Yes | Users expect cart to persist across tabs |
User authentication | ✅ Yes | Login/logout should affect all tabs |
Theme preferences | ✅ Yes | UI consistency across tabs |
Form drafts | ✅ Yes | Prevent data loss when switching tabs |
Large datasets | ❌ No | Performance overhead |
Temporary UI state | ❌ No | Tab-specific, no sync needed |
Sensitive data | ❌ No | Security considerations |
Performance Considerations
Data Size
- ✅ Keep synced data under 100KB
- ✅ Use selective syncing for large datasets
- ⚠️ Large objects slow down all tabs
Sync Frequency
- ✅ Signify sync is fast and efficient
- ✅ Debounce rapid updates if needed:
const debouncedUpdate = debounce((value) => {
sSyncedState.set(value);
}, 300);
Memory Usage
- ✅ Only active signify instances consume memory
- ✅ Sync data is shared, not duplicated per tab
Security Considerations
What's Safe to Sync
- ✅ User preferences and settings
- ✅ UI state and theme
- ✅ Shopping cart contents
- ✅ Form drafts and temporary data
What NOT to Sync
- ❌ Passwords or authentication tokens
- ❌ Personal identification information
- ❌ Payment details
- ❌ Any sensitive user data
Why It's Secure
- ✅ Work with same-origin only
- ✅ No risk from other domains
- ✅ Only same-site tabs can communicate
Browser Support
Cross-tab sync requires:
- ✅ Chrome 54+
- ✅ Firefox 38+
- ✅ Safari 15.4+
- ✅ Edge 79+
For older browsers, synced values work normally but won't sync across tabs.
Summary
Cross-tab synchronization provides:
- Instant Updates: Changes sync immediately across all tabs
- Better UX: Consistent state prevents user confusion
- Simple Setup: Just add a
syncKey
to enable sync - Automatic Conflict Resolution: Handles concurrent updates gracefully
- Performance: Efficient BroadcastChannel-based communication
Quick Checklist
- ✅ Add
syncKey
to enable sync - ✅ Use descriptive, versioned keys
- ✅ Keep synced data small and focused
- ✅ Handle external updates gracefully
- ❌ Don't sync sensitive information
- ❌ Don't sync large datasets
Perfect for: shopping carts, user auth, preferences, notifications, and any shared application state that should stay consistent across tabs.
💡 Tip: Start with simple use cases like theme preferences, then gradually add sync to more complex features as you get comfortable with the pattern.