UNPKG

use-broadcast-ts

Version:

Use the Broadcast Channel API in React easily with hooks or Zustand, and Typescript!

419 lines (372 loc) 9.81 kB
import { useState, useRef, useEffect } from 'react'; /** * Our hook will return an object with three properties: * - send: a function that will send a message to all other tabs * - state: the current state of the broadcast * - subscribe: a function that will subscribe to the broadcast (Only if options.subscribe is true) */ /** * The options for the useBroadcast hook */ /** * * @param name The name of the broadcast channel * @param val The initial value of the broadcast * @returns Returns an object with three properties: send, state and subscribe */ const useBroadcast = (name, val, options) => { /** * Store the state of the broadcast */ const [state, setState] = useState(val); /** * Store the BroadcastChannel instance */ const channel = useRef(null); /** * Store the listeners */ const listeners = useRef([]); /** * This function send the value to all the other tabs * @param val The value to send */ const send = val => { if (!channel.current) { return; } /** * Send the value to all the other tabs */ channel.current.postMessage(val); if (!(options != null && options.subscribe)) { setState(val); } /** * Dispatch the event to the listeners */ listeners.current.forEach(listener => listener(val)); }; /** * This function subscribe to the broadcast * @param callback The callback function * @returns Returns a function that unsubscribe the callback */ const subscribe = callback => { /** * Add the callback to the listeners */ listeners.current.push(callback); /** * Return a function that unsubscribe the callback */ return () => listeners.current.splice(listeners.current.indexOf(callback), 1); }; useEffect(() => { /** * If BroadcastChannel is not supported, we log an error and return */ if (typeof window === "undefined") { console.error("Window is undefined!"); return; } if (!window.BroadcastChannel) { console.error("BroadcastChannel is not supported!"); return; } /** * If the channel is null, we create a new one */ if (!channel.current) { channel.current = new BroadcastChannel(name); } /** * Subscribe to the message event * @param e The message event */ channel.current.onmessage = e => { /** * Update the state */ if (!(options != null && options.subscribe)) { setState(e.data); } /** * Dispatch an event to the listeners */ listeners.current.forEach(listener => listener(e.data)); }; /** * Cleanup */ return () => { if (!channel.current) { return; } channel.current.close(); channel.current = null; }; }, [name, options]); return { send, state, subscribe }; }; /** * The Shared type */ /** * Type implementation of the Shared function */ /** * Shared implementation * @param f Zustand state creator * @param options The options */ const sharedImpl = (f, options) => (set, get, store) => { var _options$name; /** * The broadcast channel is not supported in SSR */ if (typeof window === 'undefined' && // @ts-expect-error WorkerGlobalScope is not defined in the types !(typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)) { console.warn('BroadcastChannel is not supported in this environment. The store will not be shared.'); return f(set, get, store); } /** * If BroadcastChannel is not supported, return the basic store */ if (typeof BroadcastChannel === 'undefined') { console.warn('BroadcastChannel is not supported in this browser. The store will not be shared.'); return f(set, get, store); } /** * Types */ /** * Is the store synced with the other tabs */ let isSynced = get() !== undefined; /** * Is this tab / window the main tab / window * When a new tab / window is opened, it will be synced with the main */ let isMain = false; /** * The broadcast channel name */ const name = (_options$name = options == null ? void 0 : options.name) != null ? _options$name : f.toString(); /** * The id of the tab / window */ let id = 0; /** * Store a list of all the tabs / windows * Only for the main tab / window */ const tabs = [0]; /** * Create the broadcast channel */ const channel = new BroadcastChannel(name); const sendChangeToOtherTabs = () => { let state = get(); /** * If the partialize function is provided, use it to parse the state */ if (options != null && options.partialize) { // Partialize the state state = options.partialize(state); } /** * If the user did not specify that serialization should be skipped, remove unsupported types */ if (!(options != null && options.skipSerialization)) { // Remove unserializable types (functions, Symbols, etc.) from the state. state = JSON.parse(JSON.stringify(state)); } /** * Send the states to all the other tabs */ channel.postMessage({ action: 'change', state }); }; /** * Handle the Zustand set function * Trigger a postMessage to all the other tabs */ const onSet = (...args) => { /** * Update the states */ set(...args); /** * If the stores should not be synced, return. */ if (options != null && options.unsync) { return; } sendChangeToOtherTabs(); }; /** * Subscribe to the broadcast channel */ channel.onmessage = e => { if (e.data.action === 'sync') { /** * If this tab / window is not the main, return */ if (!isMain) { return; } sendChangeToOtherTabs(); /** * Set the new tab / window id */ const new_id = tabs[tabs.length - 1] + 1; tabs.push(new_id); options == null ? void 0 : options.onTabsChange == null ? void 0 : options.onTabsChange(tabs); channel.postMessage({ action: 'add_new_tab', id: new_id }); return; } /** * Set an id for the tab / window if it doesn't have one */ if (e.data.action === 'add_new_tab' && !isMain && id === 0) { id = e.data.id; return; } /** * On receiving a new state, update the state */ if (e.data.action === 'change') { /** * Update the state */ set(state => options != null && options.merge ? options.merge(state, e.data.state) : e.data.state); /** * Set the synced attribute */ isSynced = true; } /** * On receiving a close message, remove the tab / window id from the list */ if (e.data.action === 'close') { if (!isMain) { return; } const index = tabs.indexOf(e.data.id); if (index !== -1) { tabs.splice(index, 1); options == null ? void 0 : options.onTabsChange == null ? void 0 : options.onTabsChange(tabs); } } /** * On receiving a change_main message, change the main tab / window */ if (e.data.action === 'change_main') { if (e.data.id === id) { isMain = true; tabs.splice(0, tabs.length, ...e.data.tabs); options == null ? void 0 : options.onBecomeMain == null ? void 0 : options.onBecomeMain(id); } } }; /** * Synchronize with the main tab */ const synchronize = () => { var _options$mainTimeout; channel.postMessage({ action: 'sync' }); /** * If isSynced is false after 100ms, this tab is the main tab */ setTimeout(() => { if (!isSynced) { isMain = true; isSynced = true; options == null ? void 0 : options.onBecomeMain == null ? void 0 : options.onBecomeMain(id); } }, (_options$mainTimeout = options == null ? void 0 : options.mainTimeout) != null ? _options$mainTimeout : 100); }; /** * Handle case when the tab / window is closed */ const onClose = () => { /** * For some reason, the channel can be closed abruptly, when redirecting for example * So we need to wrap this in a try catch block */ try { channel.postMessage({ action: 'close', id }); /** * If we're closing the main, make the second the new main */ if (isMain) { /** * If there is only one tab left, close the channel and return */ if (tabs.length === 1) { /** * Clean up */ channel.close(); return; } const remaining_tabs = tabs.filter(tab => tab !== id); channel.postMessage({ action: 'change_main', id: remaining_tabs[0], tabs: remaining_tabs }); return; } } catch (e) {} }; /** * Add close event listener */ if (typeof window !== 'undefined') { window.addEventListener('beforeunload', onClose); } /** * Synchronize with the main tab */ if (!isSynced) { synchronize(); } /** * Modify and return the Zustand store */ store.setState = onSet; return f(onSet, get, store); }; /** * Shared middleware * * @example * import { create } from 'zustand'; * import { shared } from 'use-broadcast-ts'; * * const useStore = create( * shared( * (set) => ({ count: 0 }), * { name: 'my-store' } * ) * ); */ const shared = sharedImpl; export { shared, useBroadcast };