Storage Cross-Tab Communication

Communicate between browser tabs using storage

JavaScriptAdvanced
JavaScript
// Method 1: Basic cross-tab messaging
class CrossTabMessenger {
    constructor(channel) {
        this.channel = channel;
        this.listeners = [];
        this.setupListener();
    }
    
    setupListener() {
        window.addEventListener('storage', (e) => {
            if (e.key === this.channel && e.newValue) {
                const message = JSON.parse(e.newValue);
                this.listeners.forEach(listener => listener(message));
            }
        });
    }
    
    send(message) {
        localStorage.setItem(this.channel, JSON.stringify({
            ...message,
            timestamp: Date.now(),
            tabId: this.getTabId()
        }));
        // Clear immediately to allow same message again
        setTimeout(() => localStorage.removeItem(this.channel), 100);
    }
    
    onMessage(callback) {
        this.listeners.push(callback);
    }
    
    getTabId() {
        let tabId = sessionStorage.getItem('tabId');
        if (!tabId) {
            tabId = 'tab_' + Date.now() + '_' + Math.random();
            sessionStorage.setItem('tabId', tabId);
        }
        return tabId;
    }
}

const messenger = new CrossTabMessenger('app-messages');
messenger.onMessage((message) => {
    console.log('Message received:', message);
});

messenger.send({ type: 'user-updated', data: { name: 'John' } });

// Method 2: Broadcast channel API (modern)
if ('BroadcastChannel' in window) {
    const channel = new BroadcastChannel('app-channel');
    
    channel.postMessage({ type: 'notification', message: 'Hello from tab 1' });
    
    channel.onmessage = function(e) {
        console.log('Broadcast received:', e.data);
    };
}

// Method 3: Shared state manager
class SharedState {
    constructor(key) {
        this.key = key;
        this.state = this.loadState();
        this.listeners = [];
        this.setupListener();
    }
    
    loadState() {
        const stored = localStorage.getItem(this.key);
        return stored ? JSON.parse(stored) : {};
    }
    
    setupListener() {
        window.addEventListener('storage', (e) => {
            if (e.key === this.key) {
                this.state = JSON.parse(e.newValue);
                this.notifyListeners();
            }
        });
    }
    
    setState(updates) {
        this.state = { ...this.state, ...updates };
        localStorage.setItem(this.key, JSON.stringify(this.state));
        this.notifyListeners();
    }
    
    getState() {
        return this.state;
    }
    
    subscribe(callback) {
        this.listeners.push(callback);
    }
    
    notifyListeners() {
        this.listeners.forEach(listener => listener(this.state));
    }
}

const sharedState = new SharedState('app-state');
sharedState.subscribe((state) => {
    console.log('State updated:', state);
});

sharedState.setState({ user: 'John', theme: 'dark' });

// Method 4: Leader election
class TabLeader {
    constructor() {
        this.leaderKey = 'tab-leader';
        this.heartbeatInterval = null;
        this.checkLeader();
    }
    
    checkLeader() {
        const leader = localStorage.getItem(this.leaderKey);
        const now = Date.now();
        
        if (!leader) {
            this.becomeLeader();
        } else {
            const [leaderTabId, timestamp] = leader.split(':');
            const age = now - parseInt(timestamp);
            
            // If leader is inactive for 5 seconds, take over
            if (age > 5000) {
                this.becomeLeader();
            }
        }
        
        // Check every second
        setTimeout(() => this.checkLeader(), 1000);
    }
    
    becomeLeader() {
        const tabId = this.getTabId();
        localStorage.setItem(this.leaderKey, tabId + ':' + Date.now());
        this.startHeartbeat();
        console.log('Became leader');
    }
    
    startHeartbeat() {
        this.heartbeatInterval = setInterval(() => {
            const tabId = this.getTabId();
            localStorage.setItem(this.leaderKey, tabId + ':' + Date.now());
        }, 2000);
    }
    
    isLeader() {
        const leader = localStorage.getItem(this.leaderKey);
        if (!leader) return false;
        const [leaderTabId] = leader.split(':');
        return leaderTabId === this.getTabId();
    }
    
    getTabId() {
        let tabId = sessionStorage.getItem('tabId');
        if (!tabId) {
            tabId = 'tab_' + Date.now();
            sessionStorage.setItem('tabId', tabId);
        }
        return tabId;
    }
}

const tabLeader = new TabLeader();

Output

Message received: { type: 'user-updated', data: { name: 'John' }, timestamp: 1234567890, tabId: 'tab_1234567890' }
Broadcast received: { type: 'notification', message: 'Hello from tab 1' }
State updated: { user: 'John', theme: 'dark' }
Became leader

Cross-tab communication syncs data.

Methods

  • Storage events: Cross-tab
  • BroadcastChannel: Modern API
  • Shared state: Common pattern
  • Leader election: Coordination

Storage Events

  • Fires in other tabs
  • Not in same tab
  • Use for messaging

BroadcastChannel

  • Modern API
  • Better performance
  • Easier to use
  • Browser support varies

Use Cases

  • Sync user state
  • Notifications
  • Real-time updates
  • Coordination

Best Practices

  • Use BroadcastChannel if available
  • Fallback to storage events
  • Handle errors
  • Clean up listeners