Skip to content

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:

tsx
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

tsx
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

tsx
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

tsx
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:

tsx
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:

tsx
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

tsx
// ✅ 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

tsx
// ❌ 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 CaseSync?Why
Shopping cart✅ YesUsers expect cart to persist across tabs
User authentication✅ YesLogin/logout should affect all tabs
Theme preferences✅ YesUI consistency across tabs
Form drafts✅ YesPrevent data loss when switching tabs
Large datasets❌ NoPerformance overhead
Temporary UI state❌ NoTab-specific, no sync needed
Sensitive data❌ NoSecurity 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:
tsx
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:

  1. Instant Updates: Changes sync immediately across all tabs
  2. Better UX: Consistent state prevents user confusion
  3. Simple Setup: Just add a syncKey to enable sync
  4. Automatic Conflict Resolution: Handles concurrent updates gracefully
  5. 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.

Released under the MIT License.