Skip to content

useBroadcastChannel Hook ​

Hook personalizzato per sincronizzare dati tra tab/finestre usando l'API BroadcastChannel.

🎯 PerchΓ© BroadcastChannel? ​

Vantaggi rispetto a storage events: ​

  • βœ… PiΓΉ flessibile: Supporta qualsiasi tipo di dato, non solo stringhe
  • βœ… PiΓΉ performante: Comunicazione diretta senza intermediari
  • βœ… PiΓΉ pulito: API piΓΉ moderna e intuitiva
  • βœ… Type-safe: Supporta TypeScript generics
  • βœ… Bidirezionale: PuΓ² anche inviare messaggi facilmente

Limitazioni: ​

  • ⚠️ Non supportato in IE11 (ma ormai obsoleto)
  • ⚠️ Richiede fallback per browser molto vecchi

πŸ“– API ​

typescript
function useBroadcastChannel<T = any>(
    channelName: string,
    onMessage: (data: T) => void
): { postMessage: (data: T) => void }

Parametri: ​

  • channelName: Nome del canale di comunicazione (deve essere uguale in tutte le tab)
  • onMessage: Callback eseguito quando arriva un messaggio

Ritorna: ​

  • postMessage: Funzione per inviare messaggi al canale

πŸ’‘ Esempi Pratici ​

1. Autenticazione (uso reale nel progetto) ​

typescript
// AuthSyncListener.tsx
type AuthMessage = {
    type: "login" | "logout" | "company_selected" | "back_to_companies";
    timestamp: number;
};

function AuthSyncListener() {
    const navigate = useNavigate();

    const handleAuthMessage = useCallback((message: AuthMessage) => {
        if (message.type === "logout") {
            navigate("/", { replace: true });
        } else if (message.type === "login") {
            navigate("/welcome", { replace: true });
        } else if (message.type === "company_selected") {
            navigate("/welcome", { replace: true });
        } else if (message.type === "back_to_companies") {
            navigate("/companies", { replace: true });
        }
    }, [navigate]);

    useBroadcastChannel<AuthMessage>("auth", handleAuthMessage);

    return null;
}

// auth.ts
const authChannel = new BroadcastChannel("auth");

export const authService = {
    login() {
        localStorage.setItem("auth", "authenticated");
        authChannel.postMessage({ type: "login", timestamp: Date.now() });
    },

    logout() {
        localStorage.removeItem("auth");
        authChannel.postMessage({ type: "logout", timestamp: Date.now() });
    },

    setActiveCompany(companyId: number) {
        // Imposta azienda e notifica le altre tab
        authChannel.postMessage({ type: "company_selected", timestamp: Date.now() });
    },

    broadcastBackToCompanies() {
        // Notifica le altre tab del ritorno alla lista aziende
        authChannel.postMessage({ type: "back_to_companies", timestamp: Date.now() });
    }
};

2. Sincronizzazione Tema ​

typescript
type ThemeMessage = {
    theme: "light" | "dark";
    userId: string;
};

function ThemeSyncListener() {
    const { postMessage } = useBroadcastChannel<ThemeMessage>("theme", (data) => {
        document.body.classList.toggle("dark", data.theme === "dark");
        console.log(`Tema cambiato da user ${data.userId}`);
    });

    // Invia cambio tema
    const setTheme = (theme: "light" | "dark") => {
        postMessage({ theme, userId: currentUserId });
    };

    return <button onClick={() => setTheme("dark")}>Dark Mode</button>;
}

3. Notifiche Real-time ​

typescript
type NotificationMessage = {
    id: string;
    title: string;
    message: string;
    type: "info" | "success" | "error";
    timestamp: number;
};

function NotificationSystem() {
    const [notifications, setNotifications] = useState<NotificationMessage[]>([]);

    const { postMessage } = useBroadcastChannel<NotificationMessage>(
        "notifications",
        (notification) => {
            setNotifications(prev => [...prev, notification]);
            // Mostra toast
            toast(notification.message, { type: notification.type });
        }
    );

    const sendNotification = (message: string) => {
        postMessage({
            id: crypto.randomUUID(),
            title: "Nuova notifica",
            message,
            type: "info",
            timestamp: Date.now()
        });
    };

    return (
        <div>
            <button onClick={() => sendNotification("Test!")}>
                Invia notifica a tutte le tab
            </button>
            <div>
                {notifications.map(n => (
                    <div key={n.id}>{n.message}</div>
                ))}
            </div>
        </div>
    );
}

4. Carrello Condiviso ​

typescript
type CartMessage = {
    action: "add" | "remove" | "clear";
    productId?: string;
    quantity?: number;
};

function CartSync() {
    const [cart, setCart] = useState<CartItem[]>([]);

    const { postMessage } = useBroadcastChannel<CartMessage>(
        "shopping-cart",
        (message) => {
            switch (message.action) {
                case "add":
                    setCart(prev => addToCart(prev, message.productId!, message.quantity!));
                    break;
                case "remove":
                    setCart(prev => removeFromCart(prev, message.productId!));
                    break;
                case "clear":
                    setCart([]);
                    break;
            }
        }
    );

    const addProduct = (productId: string, quantity: number) => {
        // Aggiorna locale
        setCart(prev => addToCart(prev, productId, quantity));
        // Sincronizza altre tab
        postMessage({ action: "add", productId, quantity });
    };

    return <div>Cart Items: {cart.length}</div>;
}

5. Stato Condiviso Complesso ​

typescript
type AppStateMessage = {
    type: "update" | "reset";
    state?: Partial<AppState>;
    timestamp: number;
};

function useSharedState<T>(channelName: string, initialState: T) {
    const [state, setState] = useState<T>(initialState);

    const { postMessage } = useBroadcastChannel<AppStateMessage>(
        channelName,
        (message) => {
            if (message.type === "update" && message.state) {
                setState(prev => ({ ...prev, ...message.state }));
            } else if (message.type === "reset") {
                setState(initialState);
            }
        }
    );

    const updateSharedState = (updates: Partial<T>) => {
        setState(prev => ({ ...prev, ...updates }));
        postMessage({
            type: "update",
            state: updates,
            timestamp: Date.now()
        });
    };

    return [state, updateSharedState] as const;
}

// Uso:
const [appState, setAppState] = useSharedState("app-state", {
    user: null,
    preferences: {},
    theme: "light"
});

πŸ”§ Best Practices ​

1. Type Safety con TypeScript ​

typescript
// Definisci tipi per i messaggi
type Message =
    | { type: "login"; userId: string }
    | { type: "logout" }
    | { type: "update"; data: any };

useBroadcastChannel<Message>("channel", (msg) => {
    // TypeScript sa che msg puΓ² essere uno dei tre tipi
    if (msg.type === "login") {
        console.log(msg.userId); // βœ… Type-safe
    }
});

2. Include Timestamp ​

typescript
// Aiuta a debug e tracking
postMessage({
    type: "action",
    timestamp: Date.now()
});

3. Gestisci Errori ​

typescript
useEffect(() => {
    if (typeof BroadcastChannel === "undefined") {
        console.warn("BroadcastChannel non supportato");
        // Usa fallback (es. storage events)
        return;
    }
    // ...
}, []);

4. Usa useCallback per Handlers ​

typescript
const handleMessage = useCallback((data: Message) => {
    // Evita re-creazione del listener
}, [dependencies]);

useBroadcastChannel("channel", handleMessage);

5. Pulisci Risorse ​

typescript
// L'hook gestisce automaticamente la cleanup
// Il canale viene chiuso quando il componente si smonta

⚠️ Note Tecniche ​

Come Funziona ​

  1. Creazione Canale: Quando il componente si monta, viene creato un BroadcastChannel con il nome specificato
  2. Ascolto: Il canale ascolta messaggi da altre tab/finestre che usano lo stesso nome
  3. Invio: Quando chiami postMessage, il messaggio viene inviato a TUTTE le altre istanze del canale (altre tab/finestre)
  4. Cleanup: Quando il componente si smonta, il canale viene chiuso

Differenze con storage events ​

Featurestorage eventBroadcastChannel
TriggerSolo su localStorageManuale
DatiSolo stringheQualsiasi oggetto
DirezioneSolo ricezioneBidirezionale
PerformanceMediaAlta
BrowserIE11+Chrome 54+, Firefox 38+

Quando Usare Quale ​

Usa BroadcastChannel quando:

  • Hai il controllo del browser target (app interna, PWA)
  • Hai bisogno di inviare oggetti complessi
  • Vuoi performance migliori
  • Hai bisogno di comunicazione bidirezionale

Usa storage events quando:

  • Devi supportare browser molto vecchi (IE11)
  • Stai giΓ  usando localStorage per persistenza
  • Hai solo bisogno di sincronizzare dati semplici

πŸš€ Vantaggi nel Progetto ​

Prima (storage events): ​

typescript
// auth.ts
localStorage.setItem("auth", "authenticated");
window.dispatchEvent(new Event("storage")); // Hack

// AuthSyncListener.tsx
useEffect(() => {
    const handleStorageChange = (e: StorageEvent) => {
        if (e.key === "auth" && e.newValue === "authenticated") {
            // Logica...
        }
    };
    window.addEventListener("storage", handleStorageChange);
}, []);

Dopo (BroadcastChannel): ​

typescript
// auth.ts
const channel = new BroadcastChannel("auth");
channel.postMessage({ type: "login", timestamp: Date.now() });

// AuthSyncListener.tsx
useBroadcastChannel<AuthMessage>("auth", (msg) => {
    if (msg.type === "login") navigate("/welcome");
});

PiΓΉ pulito, type-safe e performante! πŸŽ‰

Documentazione Elerama Frontend